diff --git a/.woodpecker.yml b/.woodpecker.yml index 517640d6e..84dd99c80 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -2,7 +2,7 @@ # See https://github.com/woodpecker-ci/woodpecker/issues/1677 variables: - - &rust_image "rust:1.80" + - &rust_image "rust:1.81" - &rust_nightly_image "rustlang/rust:nightly" - &install_pnpm "corepack enable pnpm" - &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin" @@ -42,14 +42,14 @@ steps: - event: [pull_request, tag] prettier_check: - image: tmknom/prettier:3.0.0 + image: tmknom/prettier:3.2.5 commands: - prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml' when: - event: pull_request toml_fmt: - image: tamasfe/taplo:0.8.1 + image: tamasfe/taplo:0.9.3 commands: - taplo format --check when: @@ -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 @@ -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 @@ -157,7 +155,7 @@ steps: CARGO_HOME: .cargo_home commands: - rustup component add clippy - - cargo clippy --workspace --tests --all-targets --features console -- -D warnings + - cargo clippy --workspace --tests --all-targets -- -D warnings when: *slow_check_paths cargo_build: @@ -177,9 +175,10 @@ steps: RUST_BACKTRACE: "1" CARGO_HOME: .cargo_home commands: + - cp crates/db_schema/src/schema.rs tmp.schema - target/lemmy_server migration --all run - <<: *install_diesel_cli - - diesel print-schema --config-file=diesel.toml > tmp.schema + - diesel print-schema - diff tmp.schema crates/db_schema/src/schema.rs when: *slow_check_paths @@ -190,6 +189,7 @@ steps: RUST_BACKTRACE: "1" CARGO_HOME: .cargo_home LEMMY_TEST_FAST_FEDERATION: "1" + LEMMY_CONFIG_LOCATION: ../../config/config.hjson commands: # Install pg_dump for the schema setup test (must match server version) - apt update && apt install -y lsb-release @@ -197,12 +197,20 @@ steps: - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - - apt update && apt install -y postgresql-client-16 # Run tests - - 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 + 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" @@ -227,10 +235,13 @@ steps: publish_release_docker: image: woodpeckerci/plugin-docker-buildx - secrets: [docker_username, docker_password] settings: repo: dessalines/lemmy dockerfile: docker/Dockerfile + username: + from_secret: docker_username + password: + from_secret: docker_password platforms: linux/amd64, linux/arm64 build_args: - RUST_RELEASE_MODE=release @@ -240,10 +251,13 @@ steps: nightly_build: image: woodpeckerci/plugin-docker-buildx - secrets: [docker_username, docker_password] settings: repo: dessalines/lemmy dockerfile: docker/Dockerfile + username: + from_secret: docker_username + password: + from_secret: docker_password platforms: linux/amd64,linux/arm64 build_args: - RUST_RELEASE_MODE=release diff --git a/Cargo.lock b/Cargo.lock index e5d8ec578..38fb48f67 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" @@ -16,9 +10,9 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772" [[package]] name = "activitypub_federation" -version = "0.5.8" +version = "0.6.0-alpha2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86eea7a032da501fe07b04a83c10716ea732c45e6943d7f045bc053aca03f2a" +checksum = "4877d467ddf2fac85e9ee33aba6f2560df14125b8bfa864f85ab40e9b87753a9" dependencies = [ "activitystreams-kinds", "actix-web", @@ -32,6 +26,7 @@ dependencies = [ "futures", "futures-core", "http 0.2.12", + "http 1.1.0", "http-signature-normalization", "http-signature-normalization-reqwest", "httpdate", @@ -41,8 +36,8 @@ dependencies = [ "pin-project-lite", "rand", "regex", - "reqwest 0.11.27", - "reqwest-middleware 0.2.5", + "reqwest 0.12.8", + "reqwest-middleware", "rsa", "serde", "serde_json", @@ -95,27 +90,11 @@ dependencies = [ "smallvec", ] -[[package]] -name = "actix-form-data" -version = "0.7.0-beta.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a4f1e31610d53f56cb38c07fd8e10033e8462091528f3af6970f81a27ef9bba" -dependencies = [ - "actix-multipart", - "actix-web", - "futures-core", - "mime", - "streem", - "thiserror", - "tokio", - "tracing", -] - [[package]] name = "actix-http" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" +checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" dependencies = [ "actix-codec", "actix-rt", @@ -132,7 +111,7 @@ dependencies = [ "encoding_rs", "flate2", "futures-core", - "h2 0.3.26", + "h2", "http 0.2.12", "httparse", "httpdate", @@ -158,30 +137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.72", -] - -[[package]] -name = "actix-multipart" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-core", - "futures-util", - "httparse", - "local-waker", - "log", - "memchr", - "mime", - "rand", - "serde", - "serde_json", - "serde_plain", - "tokio", + "syn 2.0.77", ] [[package]] @@ -210,16 +166,16 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" +checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "futures-util", - "mio 0.8.11", + "mio", "socket2", "tokio", "tracing", @@ -267,9 +223,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.8.0" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" +checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" dependencies = [ "actix-codec", "actix-http", @@ -290,6 +246,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", + "impl-more", "itoa", "language-tags", "log", @@ -315,7 +272,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -335,9 +292,9 @@ dependencies = [ [[package]] name = "actix-web-prom" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76743e67d4e7efa9fc2ac7123de0dd7b2ca592668e19334f1d81a3b077afc6ac" +checksum = "56a34f1825c3ae06567a9d632466809bbf34963c86002e8921b64f32d48d289d" dependencies = [ "actix-web", "futures-core", @@ -350,9 +307,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] @@ -363,6 +320,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.11" @@ -373,7 +336,7 @@ dependencies = [ "getrandom", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -472,9 +435,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" dependencies = [ "backtrace", ] @@ -519,58 +482,30 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-stream" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] name = "atom_syndication" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30" +checksum = "2a3a5ed3201df5658d1aa45060c5a57dc9dba8a8ada20d696d67cb0c479ee043" dependencies = [ "chrono", "derive_builder", "diligent-date-parser", "never", - "quick-xml 0.31.0", + "quick-xml 0.36.1", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.3.0" @@ -579,22 +514,21 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" +checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" dependencies = [ "aws-lc-sys", "mirai-annotations", "paste", - "untrusted 0.7.1", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" +checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9" dependencies = [ "bindgen", "cc", @@ -605,119 +539,21 @@ dependencies = [ "paste", ] -[[package]] -name = "axum" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" -dependencies = [ - "async-trait", - "axum-core 0.3.4", - "bitflags 1.3.2", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper 0.1.2", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" -dependencies = [ - "async-trait", - "axum-core 0.4.3", - "bytes", - "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper 1.0.1", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "mime", - "rustversion", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 0.1.2", - "tower-layer", - "tower-service", -] - [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.0", "object", "rustc-demangle", + "windows-targets 0.52.6", ] -[[package]] -name = "barrel" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9e605929a6964efbec5ac0884bd0fe93f12a3b1eb271f52c251316640c68d9" - [[package]] name = "base32" version = "0.4.0" @@ -748,18 +584,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "bb8" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10cf871f3ff2ce56432fddc2615ac7acc3aa22ca321f8fea800846fbb32f188" -dependencies = [ - "async-trait", - "futures-util", - "parking_lot 0.12.3", - "tokio", -] - [[package]] name = "bcder" version = "0.7.4" @@ -809,9 +633,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", - "syn 2.0.72", + "syn 2.0.77", "which", ] @@ -841,9 +665,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "block-buffer" @@ -864,12 +685,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "blurhash-update" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb5ea45aeb912f2dd334834e64ecf674a6673d57c73e9d12de0298b9bf98ee8" - [[package]] name = "brotli" version = "6.0.0" @@ -899,9 +714,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" [[package]] name = "byteorder" @@ -911,9 +726,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "bytestring" @@ -940,12 +755,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.7" +version = "1.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -1017,9 +833,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.13" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" dependencies = [ "clap_builder", "clap_derive", @@ -1027,9 +843,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.13" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" dependencies = [ "anstream", "anstyle", @@ -1039,14 +855,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1055,6 +871,20 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "clearurls" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e291c00af89ac0a5b400d9ba46a682e38015ae3cd8926dbbe85b3b864d550be3" +dependencies = [ + "linkify", + "percent-encoding", + "regex", + "serde", + "serde_json", + "url", +] + [[package]] name = "clokwerk" version = "0.4.0" @@ -1066,40 +896,13 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" dependencies = [ "cc", ] -[[package]] -name = "color-eyre" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -1131,98 +934,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "config" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" -dependencies = [ - "lazy_static", - "nom", - "pathdiff", - "ron", - "serde", - "serde_json", - "toml 0.8.19", - "yaml-rust", -] - -[[package]] -name = "console-api" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" -dependencies = [ - "futures-core", - "prost 0.12.6", - "prost-types 0.12.6", - "tonic 0.10.2", - "tracing-core", -] - -[[package]] -name = "console-api" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ed14aa9c9f927213c6e4f3ef75faaad3406134efe84ba2cb7983431d5f0931" -dependencies = [ - "futures-core", - "prost 0.13.1", - "prost-types 0.13.1", - "tonic 0.12.1", - "tracing-core", -] - -[[package]] -name = "console-subscriber" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" -dependencies = [ - "console-api 0.6.0", - "crossbeam-channel", - "crossbeam-utils", - "futures-task", - "hdrhistogram", - "humantime", - "prost-types 0.12.6", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic 0.10.2", - "tracing", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "console-subscriber" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e3a111a37f3333946ebf9da370ba5c5577b18eb342ec683eb488dd21980302" -dependencies = [ - "console-api 0.8.0", - "crossbeam-channel", - "crossbeam-utils", - "futures-task", - "hdrhistogram", - "humantime", - "hyper-util", - "prost 0.13.1", - "prost-types 0.13.1", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic 0.12.1", - "tracing", - "tracing-core", - "tracing-subscriber", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1231,18 +942,18 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.32" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.32" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" dependencies = [ "proc-macro2", "quote", @@ -1284,15 +995,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -1385,7 +1096,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1407,20 +1118,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.72", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.10", + "syn 2.0.77", ] [[package]] @@ -1463,23 +1161,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", - "der_derive", - "flagset", "pem-rfc7468", "zeroize", ] -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "deranged" version = "0.3.11" @@ -1503,44 +1188,44 @@ dependencies = [ [[package]] name = "derive-new" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] name = "derive_builder" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] name = "derive_builder_macro" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1553,7 +1238,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1578,7 +1263,6 @@ dependencies = [ "itoa", "pq-sys", "serde_json", - "time", "uuid", ] @@ -1589,7 +1273,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acada1517534c92d3f382217b485db8a8638f111b0e3f2a2a8e26165050f77be" dependencies = [ "async-trait", - "bb8", "deadpool 0.9.5", "diesel", "futures-util", @@ -1598,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" @@ -1607,7 +1299,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1618,7 +1310,7 @@ checksum = "d5adf688c584fe33726ce0e2898f608a2a92578ac94a4a92fcecf73214fe0716" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1630,7 +1322,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1660,7 +1352,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1690,6 +1382,17 @@ dependencies = [ "chrono", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "doku" version = "0.21.1" @@ -1728,9 +1431,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" @@ -1801,7 +1504,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -1865,31 +1568,12 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" -[[package]] -name = "fallible_collections" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd" -dependencies = [ - "hashbrown 0.13.2", -] - [[package]] name = "fancy-regex" version = "0.11.0" @@ -1902,9 +1586,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fdeflate" @@ -1915,20 +1599,14 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "flagset" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" - [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -1937,21 +1615,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1967,16 +1630,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "fs_extra" version = "1.3.0" @@ -1995,9 +1648,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -2010,9 +1663,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -2020,15 +1673,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -2037,38 +1690,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -2082,15 +1735,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -2116,9 +1760,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "glob" @@ -2138,26 +1782,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.3.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.1.0", - "indexmap 2.3.0", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -2170,15 +1795,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -2189,19 +1805,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "hdrhistogram" -version = "7.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" -dependencies = [ - "base64 0.21.7", - "byteorder", - "flate2", - "nom", - "num-traits", -] - [[package]] name = "heck" version = "0.4.1" @@ -2275,9 +1878,9 @@ dependencies = [ [[package]] name = "html2text" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c66ee488a63a92237d5b48875b7e05bb293be8fb2894641c8118b60c08ab5ef" +checksum = "042a9677c258ac2952dd026bb0cd21972f00f644a5a38f5a215cb22cdaf6834e" dependencies = [ "html5ever 0.27.0", "markup5ever 0.12.1", @@ -2311,7 +1914,7 @@ dependencies = [ "markup5ever 0.12.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -2381,16 +1984,16 @@ dependencies = [ [[package]] name = "http-signature-normalization-reqwest" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cfb84663420ec12c4422820bfdf5e8e5e57467892587f43ac432e73ebce880" +checksum = "b8822f7eab343cae1ce3bd3b6d0b9b58c72adaf3463627cfe150f8f5406f27aa" dependencies = [ "async-trait", - "base64 0.13.1", + "base64 0.22.1", "http-signature-normalization", "httpdate", - "reqwest 0.11.27", - "reqwest-middleware 0.2.5", + "reqwest 0.12.8", + "reqwest-middleware", "sha2", "thiserror", "tokio", @@ -2408,12 +2011,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.30" @@ -2424,7 +2021,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2447,11 +2044,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.5", "http 1.1.0", "http-body 1.0.1", "httparse", - "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -2475,65 +2070,27 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", "hyper 1.4.1", "hyper-util", - "rustls 0.23.12", + "rustls 0.23.14", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", "tower-service", - "webpki-roots 0.26.3", -] - -[[package]] -name = "hyper-timeout" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" -dependencies = [ - "hyper 0.14.30", - "pin-project-lite", - "tokio", - "tokio-io-timeout", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" -dependencies = [ - "hyper 1.4.1", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.30", - "native-tls", - "tokio", - "tokio-native-tls", + "webpki-roots 0.26.5", ] [[package]] name = "hyper-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", @@ -2566,14 +2123,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8215279f83f9b829403812f845aa2d0dd5966332aa2fd0334a375256f3dd0322" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2592,6 +2149,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2618,6 +2293,18 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", +] + [[package]] name = "image" version = "0.24.9" @@ -2637,12 +2324,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" -[[package]] -name = "indenter" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" - [[package]] name = "indexmap" version = "1.9.3" @@ -2656,9 +2337,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -2674,20 +2355,11 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "is_terminal_polyfill" @@ -2695,15 +2367,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -2759,9 +2422,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -2804,7 +2467,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lemmy_api" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -2834,7 +2497,7 @@ dependencies = [ [[package]] name = "lemmy_api_common" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -2854,8 +2517,8 @@ dependencies = [ "moka", "pretty_assertions", "regex", - "reqwest 0.11.27", - "reqwest-middleware 0.2.5", + "reqwest 0.12.8", + "reqwest-middleware", "rosetta-i18n", "serde", "serde_with", @@ -2871,13 +2534,14 @@ dependencies = [ [[package]] name = "lemmy_api_crud" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "accept-language", "activitypub_federation", "actix-web", "anyhow", "bcrypt", + "chrono", "futures", "lemmy_api_common", "lemmy_db_schema", @@ -2885,6 +2549,9 @@ dependencies = [ "lemmy_db_views_actor", "lemmy_utils", "moka", + "serde", + "serde_json", + "serde_with", "tracing", "url", "uuid", @@ -2893,7 +2560,7 @@ dependencies = [ [[package]] name = "lemmy_apub" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -2906,7 +2573,6 @@ dependencies = [ "futures", "html2md", "html2text", - "http 0.2.12", "itertools 0.13.0", "lemmy_api_common", "lemmy_db_schema", @@ -2915,7 +2581,7 @@ dependencies = [ "lemmy_utils", "moka", "pretty_assertions", - "reqwest 0.11.27", + "reqwest 0.12.8", "serde", "serde_json", "serde_with", @@ -2930,7 +2596,7 @@ dependencies = [ [[package]] name = "lemmy_db_perf" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "anyhow", "clap", @@ -2945,7 +2611,7 @@ dependencies = [ [[package]] name = "lemmy_db_schema" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "anyhow", @@ -2956,6 +2622,7 @@ dependencies = [ "derive-new", "diesel", "diesel-async", + "diesel-bind-if-some", "diesel-derive-enum", "diesel-derive-newtype", "diesel_ltree", @@ -2967,7 +2634,7 @@ dependencies = [ "moka", "pretty_assertions", "regex", - "rustls 0.23.12", + "rustls 0.23.14", "serde", "serde_json", "serde_with", @@ -2978,14 +2645,14 @@ dependencies = [ "tokio-postgres-rustls", "tracing", "ts-rs", - "typed-builder", + "tuplex", "url", "uuid", ] [[package]] name = "lemmy_db_views" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "actix-web", "chrono", @@ -3007,7 +2674,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_actor" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "chrono", "diesel", @@ -3027,7 +2694,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_moderator" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "diesel", "diesel-async", @@ -3039,7 +2706,7 @@ dependencies = [ [[package]] name = "lemmy_federate" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -3056,7 +2723,7 @@ dependencies = [ "lemmy_utils", "mockall", "moka", - "reqwest 0.11.27", + "reqwest 0.12.8", "serde_json", "serial_test", "test-context", @@ -3070,31 +2737,31 @@ dependencies = [ [[package]] name = "lemmy_routes" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", "anyhow", "chrono", "futures", + "http 1.1.0", "lemmy_api_common", "lemmy_db_schema", "lemmy_db_views", "lemmy_db_views_actor", "lemmy_utils", - "reqwest 0.11.27", - "reqwest-middleware 0.2.5", + "reqwest 0.12.8", + "reqwest-middleware", "rss", "serde", "tokio", "tracing", "url", - "urlencoding", ] [[package]] name = "lemmy_server" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-cors", @@ -3103,7 +2770,6 @@ dependencies = [ "chrono", "clap", "clokwerk", - "console-subscriber 0.4.0", "diesel", "diesel-async", "futures-util", @@ -3115,48 +2781,46 @@ dependencies = [ "lemmy_federate", "lemmy_routes", "lemmy_utils", - "opentelemetry 0.19.0", - "opentelemetry-otlp 0.12.0", - "pict-rs", "pretty_assertions", "prometheus", - "reqwest 0.11.27", - "reqwest-middleware 0.2.5", - "reqwest-tracing 0.4.8", - "rustls 0.23.12", + "reqwest 0.12.8", + "reqwest-middleware", + "reqwest-tracing", + "rustls 0.23.14", "serde_json", "serial_test", "tokio", "tracing", "tracing-actix-web", - "tracing-error", - "tracing-log 0.2.0", - "tracing-opentelemetry 0.19.0", "tracing-subscriber", "url", ] [[package]] name = "lemmy_utils" -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" dependencies = [ "actix-web", "anyhow", "cfg-if", + "clearurls", "deser-hjson", "diesel", "doku", "enum-map", "futures", "html2text", - "http 0.2.12", + "http 1.1.0", "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.11.27", - "reqwest-middleware 0.2.5", + "reqwest-middleware", "rosetta-build", "rosetta-i18n", "serde", @@ -3165,7 +2829,6 @@ dependencies = [ "strum", "tokio", "tracing", - "tracing-error", "ts-rs", "url", "urlencoding", @@ -3174,9 +2837,9 @@ dependencies = [ [[package]] name = "lettre" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a62049a808f1c4e2356a2a380bd5f2aca3b011b0b482cf3b914ba1731426969" +checksum = "69f204773bab09b150320ea1c83db41dc6ee606a4bc36dc1f43005fe7b58ce06" dependencies = [ "async-trait", "base64 0.22.1", @@ -3187,25 +2850,26 @@ dependencies = [ "futures-io", "futures-util", "httpdate", - "idna 0.5.0", + "idna 1.0.2", "mime", "nom", "percent-encoding", "quoted_printable", - "rustls 0.23.12", - "rustls-pemfile 2.1.2", + "rustls 0.23.14", + "rustls-pemfile 2.1.3", + "rustls-pki-types", "socket2", "tokio", "tokio-rustls 0.26.0", "url", - "webpki-roots 0.26.3", + "webpki-roots 0.26.5", ] [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libloading" @@ -3244,6 +2908,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "local-channel" version = "0.1.5" @@ -3273,12 +2943,11 @@ dependencies = [ [[package]] name = "lodepng" -version = "3.10.2" +version = "3.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72decff904ebe3e39c88b0488985f24e9796bb4dc5bcf65beebaa5d3b7073ff4" +checksum = "7b2dea7cda68e381418c985fd8f32a9c279a21ae8c715f2376adb20c27a0fad3" dependencies = [ "crc32fast", - "fallible_collections", "flate2", "libc", "rgb", @@ -3319,6 +2988,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" @@ -3380,12 +3087,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -3419,51 +3120,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "metrics" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884adb57038347dfbaf2d5065887b6cf4312330dc8e94bc30a1a839bd79d3261" -dependencies = [ - "ahash", - "portable-atomic", -] - -[[package]] -name = "metrics-exporter-prometheus" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f0c8427b39666bf970460908b213ec09b3b350f20c0c2eabcbba51704a08e6" -dependencies = [ - "base64 0.22.1", - "http-body-util", - "hyper 1.4.1", - "hyper-util", - "indexmap 2.3.0", - "ipnet", - "metrics", - "metrics-util", - "quanta", - "thiserror", - "tokio", - "tracing", -] - -[[package]] -name = "metrics-util" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4259040465c955f9f2f1a4a8a16dc46726169bca0f88e8fb2dbeced487c3e828" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "hashbrown 0.14.5", - "metrics", - "num_cpus", - "quanta", - "sketches-ddsketch", -] - [[package]] name = "migrations_internals" version = "2.1.0" @@ -3471,7 +3127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" dependencies = [ "serde", - "toml 0.7.8", + "toml", ] [[package]] @@ -3491,16 +3147,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3518,25 +3164,23 @@ dependencies = [ ] [[package]] -name = "mio" -version = "0.8.11" +name = "miniz_oxide" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", + "adler2", ] [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -3570,7 +3214,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -3587,7 +3231,7 @@ dependencies = [ "event-listener", "futures-util", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "quanta", "rustc_version", "smallvec", @@ -3603,29 +3247,6 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" - -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "never" version = "0.1.0" @@ -3733,9 +3354,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -3746,249 +3367,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "openssl" -version = "0.10.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" -dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "opentelemetry" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf9b1c4e9a6c4de793c632496fa490bdc0e1eea73f0c91394f7b6990935d22" -dependencies = [ - "async-trait", - "crossbeam-channel", - "futures", - "js-sys", - "lazy_static", - "percent-encoding", - "pin-project", - "rand", - "thiserror", -] - -[[package]] -name = "opentelemetry" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4b8347cc26099d3aeee044065ecc3ae11469796b4d65d065a23a584ed92a6f" -dependencies = [ - "opentelemetry_api", - "opentelemetry_sdk 0.19.0", -] - -[[package]] -name = "opentelemetry" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "once_cell", - "pin-project-lite", - "thiserror", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af72d59a4484654ea8eb183fea5ae4eb6a41d7ac3e3bae5f4d2a282a3a7d3ca" -dependencies = [ - "async-trait", - "futures", - "futures-util", - "http 0.2.12", - "opentelemetry 0.19.0", - "opentelemetry-proto 0.2.0", - "prost 0.11.9", - "thiserror", - "tokio", - "tonic 0.8.3", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94c69209c05319cdf7460c6d4c055ed102be242a0a6245835d7bc42c6ec7f54" -dependencies = [ - "async-trait", - "futures-core", - "http 0.2.12", - "opentelemetry 0.23.0", - "opentelemetry-proto 0.6.0", - "opentelemetry_sdk 0.23.0", - "prost 0.12.6", - "thiserror", - "tokio", - "tonic 0.11.0", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045f8eea8c0fa19f7d48e7bc3128a39c2e5c533d5c61298c548dfefc1064474c" -dependencies = [ - "futures", - "futures-util", - "opentelemetry 0.19.0", - "prost 0.11.9", - "tonic 0.8.3", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984806e6cf27f2b49282e2a05e288f30594f3dbc74eb7a6e99422bc48ed78162" -dependencies = [ - "opentelemetry 0.23.0", - "opentelemetry_sdk 0.23.0", - "prost 0.12.6", - "tonic 0.11.0", -] - -[[package]] -name = "opentelemetry_api" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed41783a5bf567688eb38372f2b7a8530f5a607a4b49d38dd7573236c23ca7e2" -dependencies = [ - "fnv", - "futures-channel", - "futures-util", - "indexmap 1.9.3", - "once_cell", - "pin-project-lite", - "thiserror", - "urlencoding", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3a2a91fdbfdd4d212c0dcc2ab540de2c2bcbbd90be17de7a7daf8822d010c1" -dependencies = [ - "async-trait", - "crossbeam-channel", - "dashmap", - "fnv", - "futures-channel", - "futures-executor", - "futures-util", - "once_cell", - "opentelemetry_api", - "percent-encoding", - "rand", - "thiserror", - "tokio", - "tokio-stream", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd" -dependencies = [ - "async-trait", - "futures-channel", - "futures-executor", - "futures-util", - "glob", - "lazy_static", - "once_cell", - "opentelemetry 0.23.0", - "ordered-float", - "percent-encoding", - "rand", - "thiserror", - "tokio", - "tokio-stream", -] - -[[package]] -name = "ordered-float" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a91171844676f8c7990ce64959210cd2eaef32c2612c50f9fae9f8aaa6065a6" -dependencies = [ - "num-traits", -] - [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -3997,21 +3386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -4022,7 +3397,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -4033,12 +3408,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - [[package]] name = "pem" version = "3.0.4" @@ -4128,7 +3497,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] @@ -4137,72 +3506,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "pict-rs" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bbee61836cce10f7cf733196b7c0701e7ea6d0b617da68a3e6e4311b6262c2b" -dependencies = [ - "actix-form-data", - "actix-web", - "async-trait", - "barrel", - "base64 0.22.1", - "bb8", - "blurhash-update", - "clap", - "color-eyre", - "config", - "console-subscriber 0.2.0", - "dashmap", - "diesel", - "diesel-async", - "diesel-derive-enum", - "futures-core", - "hex", - "md-5", - "metrics", - "metrics-exporter-prometheus", - "mime", - "opentelemetry 0.23.0", - "opentelemetry-otlp 0.16.0", - "opentelemetry_sdk 0.23.0", - "pin-project-lite", - "refinery", - "reqwest 0.12.5", - "reqwest-middleware 0.3.2", - "reqwest-tracing 0.5.2", - "rustls 0.23.12", - "rustls-channel-resolver", - "rustls-pemfile 2.1.2", - "rusty-s3", - "serde", - "serde-tuple-vec-map", - "serde_json", - "serde_urlencoded", - "sha2", - "sled", - "streem", - "subtle", - "thiserror", - "time", - "tokio", - "tokio-postgres", - "tokio-postgres-generic-rustls", - "tokio-util", - "toml 0.8.19", - "tracing", - "tracing-actix-web", - "tracing-error", - "tracing-log 0.2.0", - "tracing-opentelemetry 0.24.0", - "tracing-subscriber", - "url", - "uuid", - "webpki-roots 0.26.3", + "siphasher", ] [[package]] @@ -4222,7 +3526,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -4271,7 +3575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.3.0", + "indexmap 2.5.0", "quick-xml 0.32.0", "serde", "time", @@ -4287,27 +3591,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", -] - -[[package]] -name = "portable-atomic" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" - -[[package]] -name = "postgres" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c9ec84ab55b0f9e418675de50052d494ba893fd28c65769a6e68fcdacbee2b8" -dependencies = [ - "bytes", - "fallible-iterator", - "futures-util", - "log", - "tokio", - "tokio-postgres", + "miniz_oxide 0.7.4", ] [[package]] @@ -4330,17 +3614,13 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02048d9e032fb3cc3413bbf7b83a15d84a5d419778e2628751896d856498eee9" +checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" dependencies = [ "bytes", "fallible-iterator", "postgres-protocol", - "serde", - "serde_json", - "time", - "uuid", ] [[package]] @@ -4351,11 +3631,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy 0.6.6", + "zerocopy", ] [[package]] @@ -4401,9 +3681,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", @@ -4411,12 +3691,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -4462,99 +3742,12 @@ dependencies = [ "lazy_static", "libc", "memchr", - "parking_lot 0.12.3", + "parking_lot", "procfs", "protobuf", "thiserror", ] -[[package]] -name = "prost" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes", - "prost-derive 0.11.9", -] - -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes", - "prost-derive 0.12.6", -] - -[[package]] -name = "prost" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" -dependencies = [ - "bytes", - "prost-derive 0.13.1", -] - -[[package]] -name = "prost-derive" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "prost-derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" -dependencies = [ - "anyhow", - "itertools 0.13.0", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost 0.12.6", -] - -[[package]] -name = "prost-types" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee5168b05f49d4b0ca581206eb14a7b22fafd963efe729ac48eb03266e25cc2" -dependencies = [ - "prost 0.13.1", -] - [[package]] name = "protobuf" version = "2.28.0" @@ -4563,9 +3756,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psm" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" dependencies = [ "cc", ] @@ -4585,26 +3778,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quick-xml" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" -dependencies = [ - "encoding_rs", - "memchr", -] - [[package]] name = "quick-xml" version = "0.32.0" @@ -4615,17 +3788,28 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.11.2" +name = "quick-xml" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" dependencies = [ "bytes", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", - "rustls 0.23.12", + "rustc-hash 2.0.0", + "rustls 0.23.14", + "socket2", "thiserror", "tokio", "tracing", @@ -4633,15 +3817,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.3" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" dependencies = [ "bytes", "rand", "ring", - "rustc-hash", - "rustls 0.23.12", + "rustc-hash 2.0.0", + "rustls 0.23.14", "slab", "thiserror", "tinyvec", @@ -4650,21 +3834,22 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" dependencies = [ "libc", "once_cell", "socket2", - "windows-sys 0.52.0", + "tracing", + "windows-sys 0.59.0", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -4722,92 +3907,28 @@ checksum = "a25d631e41bfb5fdcde1d4e2215f62f7f0afa3ff11e26563765bd6ea1d229aeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "refinery" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0904191f0566c3d3e0091d5cc8dec22e663d77def2d247b16e7a438b188bf75d" -dependencies = [ - "refinery-core", - "refinery-macros", -] - -[[package]] -name = "refinery-core" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf253999e1899ae476c910b994959e341d84c4389ba9533d3dacbe06df04825" -dependencies = [ - "async-trait", - "cfg-if", - "log", - "postgres", - "regex", - "serde", - "siphasher 1.0.1", - "thiserror", - "time", - "tokio", - "tokio-postgres", - "toml 0.8.19", - "url", - "walkdir", -] - -[[package]] -name = "refinery-macros" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd81f69687fe8a1fa10995108b3ffc7cdbd63e682a4f8fbfd1020130780d7e17" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "refinery-core", - "regex", - "syn 2.0.72", -] - [[package]] name = "regex" -version = "1.10.5" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -4821,13 +3942,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -4844,9 +3965,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -4854,24 +3975,20 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "async-compression", "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", + "h2", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.30", "hyper-rustls 0.24.2", - "hyper-tls", "ipnet", "js-sys", "log", "mime", - "mime_guess", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -4883,34 +4000,33 @@ dependencies = [ "sync_wrapper 0.1.2", "system-configuration", "tokio", - "tokio-native-tls", "tokio-rustls 0.24.1", - "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", "webpki-roots 0.25.4", - "winreg 0.50.0", + "winreg", ] [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ + "async-compression", "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "http 1.1.0", "http-body 1.0.1", "http-body-util", "hyper 1.4.1", - "hyper-rustls 0.27.2", + "hyper-rustls 0.27.3", "hyper-util", "ipnet", "js-sys", @@ -4920,8 +4036,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.12", - "rustls-pemfile 2.1.2", + "rustls 0.23.14", + "rustls-pemfile 2.1.3", "rustls-pki-types", "serde", "serde_json", @@ -4936,35 +4052,20 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.3", - "winreg 0.52.0", + "webpki-roots 0.26.5", + "windows-registry", ] [[package]] name = "reqwest-middleware" -version = "0.2.5" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a735987236a8e238bf0296c7e351b999c188ccc11477f311b82b55c93984216" -dependencies = [ - "anyhow", - "async-trait", - "http 0.2.12", - "reqwest 0.11.27", - "serde", - "task-local-extensions", - "thiserror", -] - -[[package]] -name = "reqwest-middleware" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39346a33ddfe6be00cbc17a34ce996818b97b230b87229f10114693becca1268" +checksum = "562ceb5a604d3f7c885a792d42c199fd8af239d0a51b2fa6a78aafa092452b04" dependencies = [ "anyhow", "async-trait", "http 1.1.0", - "reqwest 0.12.5", + "reqwest 0.12.8", "serde", "thiserror", "tower-service", @@ -4972,35 +4073,17 @@ dependencies = [ [[package]] name = "reqwest-tracing" -version = "0.4.8" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190838e54153d7a7e2ea98851304b3ce92daeabf14c54d32b01b84a3e636f683" -dependencies = [ - "anyhow", - "async-trait", - "getrandom", - "matchit 0.7.3", - "opentelemetry 0.16.0", - "reqwest 0.11.27", - "reqwest-middleware 0.2.5", - "task-local-extensions", - "tracing", - "tracing-opentelemetry 0.16.0", -] - -[[package]] -name = "reqwest-tracing" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e45dcad05dc210fdb0278d62a679eb768730af808f8cb552f810da89bdbe76d" +checksum = "bfdd9bfa64c72233d8dd99ab7883efcdefe9e16d46488ecb9228b71a2e2ceb45" dependencies = [ "anyhow", "async-trait", "getrandom", "http 1.1.0", - "matchit 0.8.4", - "reqwest 0.12.5", - "reqwest-middleware 0.3.2", + "matchit", + "reqwest 0.12.8", + "reqwest-middleware", "tracing", ] @@ -5012,9 +4095,9 @@ checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" [[package]] name = "rgb" -version = "0.8.45" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade4539f42266ded9e755c605bdddf546242b2c961b03b06a7375260788a0523" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -5030,22 +4113,10 @@ dependencies = [ "getrandom", "libc", "spin", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.6.0", - "serde", - "serde_derive", -] - [[package]] name = "rosetta-build" version = "0.1.3" @@ -5088,14 +4159,14 @@ dependencies = [ [[package]] name = "rss" -version = "2.0.8" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f374fd66bb795938b78c021db1662d43a8ffbc42ec1ac25429fc4833b732751" +checksum = "27e92048f840d98c6d6dd870af9101610ea9ff413f11f1bcebf4f4c31d96d957" dependencies = [ "atom_syndication", "derive_builder", "never", - "quick-xml 0.31.0", + "quick-xml 0.36.1", ] [[package]] @@ -5111,19 +4182,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "rustc_version" -version = "0.4.0" +name = "rustc-hash" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", @@ -5146,30 +4223,20 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.6", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] -[[package]] -name = "rustls-channel-resolver" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fede2a247359da6b4998f7723ec6468c2d6a577a5d8c17e54f21806426ad2290" -dependencies = [ - "nanorand", - "rustls 0.23.12", -] - [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -5181,9 +4248,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -5191,9 +4258,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" @@ -5202,19 +4269,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", - "untrusted 0.9.0", + "untrusted", ] [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -5223,25 +4290,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -[[package]] -name = "rusty-s3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31aa883f1b986a5249641e574ca0e11ac4fb9970b009c6fbb96fedaf4fa78db8" -dependencies = [ - "base64 0.21.7", - "hmac", - "md-5", - "percent-encoding", - "quick-xml 0.30.0", - "serde", - "serde_json", - "sha2", - "time", - "url", - "zeroize", -] - [[package]] name = "ryu" version = "1.0.18" @@ -5259,22 +4307,13 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.6" +version = "2.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ccfb12511cdb770157ace92d7dda771e498445b78f9886e8cdbc5140a4eced" +checksum = "0c947adb109a8afce5fc9c7bf951f87f146e9147b3a6a58413105628fb1d1e66" dependencies = [ "sdd", ] -[[package]] -name = "schannel" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "scoped-futures" version = "0.1.3" @@ -5298,37 +4337,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", - "untrusted 0.9.0", + "untrusted", ] [[package]] name = "sdd" -version = "2.1.0" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177258b64c0faaa9ffd3c65cd3262c2bc7e2588dbbd9c1641d0346145c1bbda8" - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.6.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "60a7b59a5d9b0099720b417b6325d91a52cbf5b3dcb5041d864be53eefa58abc" [[package]] name = "select" @@ -5349,55 +4365,37 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] -[[package]] -name = "serde-tuple-vec-map" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a04d0ebe0de77d7d445bb729a895dcb0a288854b267ca85f030ce51cdc578c82" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.5.0", "itoa", "memchr", "ryu", "serde", ] -[[package]] -name = "serde_plain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "0.6.7" @@ -5421,15 +4419,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.3.0", + "indexmap 2.5.0", "serde", "serde_derive", "serde_json", @@ -5439,14 +4437,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -5458,7 +4456,7 @@ dependencies = [ "futures", "log", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "scc", "serial_test_derive", ] @@ -5471,7 +4469,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -5554,12 +4552,6 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "sitemap-rs" version = "0.2.1" @@ -5570,12 +4562,6 @@ dependencies = [ "xml-builder", ] -[[package]] -name = "sketches-ddsketch" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" - [[package]] name = "slab" version = "0.4.9" @@ -5585,22 +4571,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - [[package]] name = "smallvec" version = "1.13.2" @@ -5615,7 +4585,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -5645,26 +4615,22 @@ dependencies = [ ] [[package]] -name = "stacker" -version = "0.1.15" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" dependencies = [ "cc", "cfg-if", "libc", "psm", - "winapi", -] - -[[package]] -name = "streem" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8b0c8184b0fe05b37dd75d66205195cd57563c6c87cb92134a025a34a6ab34" -dependencies = [ - "futures-core", - "pin-project-lite", + "windows-sys 0.59.0", ] [[package]] @@ -5681,7 +4647,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -5747,7 +4713,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -5769,9 +4735,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -5789,6 +4755,20 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] [[package]] name = "syntect" @@ -5803,7 +4783,7 @@ dependencies = [ "fnv", "once_cell", "plist", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "serde", "serde_derive", "serde_json", @@ -5839,27 +4819,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "task-local-extensions" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba323866e5d033818e3240feeb9f7db2c4296674e4d9e16b97b7bf8f490434e8" -dependencies = [ - "pin-utils", -] - -[[package]] -name = "tempfile" -version = "3.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" -dependencies = [ - "cfg-if", - "fastrand", - "rustix", - "windows-sys 0.52.0", -] - [[package]] name = "tendril" version = "0.4.3" @@ -5904,7 +4863,7 @@ checksum = "78ea17a2dc368aeca6f554343ced1b1e31f76d63683fa8016e5844bd7a5144a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -5924,7 +4883,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -5974,6 +4933,16 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -5989,56 +4958,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tls_codec" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "tokio" -version = "1.39.2" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.1", - "parking_lot 0.12.3", + "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "tracing", "windows-sys 0.52.0", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-macros" version = "2.4.0" @@ -6047,24 +4984,14 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", + "syn 2.0.77", ] [[package]] name = "tokio-postgres" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03adcf0147e203b6032c0b2d30be1415ba03bc348901f3ff1cc0df6a733e60c3" +checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" dependencies = [ "async-trait", "byteorder", @@ -6073,7 +5000,7 @@ dependencies = [ "futures-channel", "futures-util", "log", - "parking_lot 0.12.3", + "parking_lot", "percent-encoding", "phf 0.11.2", "pin-project-lite", @@ -6086,20 +5013,6 @@ dependencies = [ "whoami", ] -[[package]] -name = "tokio-postgres-generic-rustls" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e98c31c29b2666fb28720739e11476166be4ead1610a37dcd7414bb124413a" -dependencies = [ - "aws-lc-rs", - "rustls 0.23.12", - "tokio", - "tokio-postgres", - "tokio-rustls 0.26.0", - "x509-cert", -] - [[package]] name = "tokio-postgres-rustls" version = "0.12.0" @@ -6107,7 +5020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" dependencies = [ "ring", - "rustls 0.23.12", + "rustls 0.23.14", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", @@ -6130,27 +5043,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.12", + "rustls 0.23.14", "rustls-pki-types", "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -6168,19 +5070,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.19.15", -] - -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.22.20", + "toml_edit", ] [[package]] @@ -6198,140 +5088,11 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" -dependencies = [ - "indexmap 2.3.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.6.18", -] - -[[package]] -name = "tonic" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.6.20", - "base64 0.13.1", - "bytes", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", - "hyper-timeout 0.4.1", - "percent-encoding", - "pin-project", - "prost 0.11.9", - "prost-derive 0.11.9", - "tokio", - "tokio-stream", - "tokio-util", - "tower", - "tower-layer", - "tower-service", - "tracing", - "tracing-futures", -] - -[[package]] -name = "tonic" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.6.20", - "base64 0.21.7", - "bytes", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", - "hyper-timeout 0.4.1", - "percent-encoding", - "pin-project", - "prost 0.12.6", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.6.20", - "base64 0.21.7", - "bytes", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", - "hyper-timeout 0.4.1", - "percent-encoding", - "pin-project", - "prost 0.12.6", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38659f4a91aba8598d27821589f5db7dddd94601e7a01b1e485a50e5484c7401" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.5", - "base64 0.22.1", - "bytes", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.4.1", - "hyper-timeout 0.5.1", - "hyper-util", - "percent-encoding", - "pin-project", - "prost 0.13.1", - "socket2", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", + "winnow", ] [[package]] @@ -6358,29 +5119,24 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand", - "slab", "tokio", - "tokio-util", "tower-layer", "tower-service", - "tracing", ] [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -6396,16 +5152,14 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee9e39a66d9b615644893ffc1704d2a89b5b315b7fd0228ad3182ca9a306b19" +checksum = "284586dc201db407be8c9d721abad1b3a6dacbbce5cccecd4fd15a37db95ab0d" dependencies = [ "actix-web", "mutually_exclusive_features", - "opentelemetry 0.23.0", "pin-project", "tracing", - "tracing-opentelemetry 0.24.0", "uuid", ] @@ -6417,7 +5171,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -6430,37 +5184,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-error" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - -[[package]] -name = "tracing-log" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -6472,51 +5195,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-opentelemetry" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffbf13a0f8b054a4e59df3a173b818e9c6177c02789871f2073977fd0062076" -dependencies = [ - "opentelemetry 0.16.0", - "tracing", - "tracing-core", - "tracing-log 0.1.4", - "tracing-subscriber", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00a39dcf9bfc1742fa4d6215253b33a6e474be78275884c216fc2a06267b3600" -dependencies = [ - "once_cell", - "opentelemetry 0.19.0", - "tracing", - "tracing-core", - "tracing-log 0.1.4", - "tracing-subscriber", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f68803492bf28ab40aeccaecc7021096bd256baf7ca77c3d425d89b35a7be4e4" -dependencies = [ - "js-sys", - "once_cell", - "opentelemetry 0.23.0", - "opentelemetry_sdk 0.23.0", - "smallvec", - "tracing", - "tracing-core", - "tracing-log 0.2.0", - "tracing-subscriber", - "web-time", -] - [[package]] name = "tracing-serde" version = "0.1.3" @@ -6544,7 +5222,7 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log 0.2.0", + "tracing-log", "tracing-serde", ] @@ -6566,7 +5244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.77", ] [[package]] @@ -6583,47 +5261,34 @@ 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.72", + "syn 2.0.77", "termcolor", ] [[package]] -name = "typed-builder" -version = "0.19.1" +name = "tuplex" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06fbd5b8de54c5f7c91f6fe4cebb949be2125d7758e630bb58b1d831dbce600" -dependencies = [ - "typed-builder-macro", -] - -[[package]] -name = "typed-builder-macro" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9534daa9fd3ed0bd911d462a37f172228077e7abf18c18a5f67199d959205f8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] +checksum = "676ac81d5454c4dcf37955d34fa8626ede3490f744b86ca14a7b90168d2a08aa" [[package]] name = "typenum" @@ -6631,15 +5296,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.15" @@ -6654,9 +5310,9 @@ checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" @@ -6669,9 +5325,9 @@ dependencies = [ [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] name = "unicode-width" @@ -6681,15 +5337,9 @@ checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" [[package]] name = "untrusted" @@ -6721,12 +5371,24 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -6794,34 +5456,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -6831,9 +5494,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6841,22 +5504,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-streams" @@ -6873,19 +5536,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -6893,9 +5546,9 @@ dependencies = [ [[package]] name = "webmention" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d07b90492f7b6fe35f5298fcd01c663d3c453e8c302dc86c7292c6681b8117d" +checksum = "c2c1a8d1f70dd7b5b5e2bf5fca4dd97fa5ed4e8adcf0b0ee4c6ebe1ebac7a2fe" dependencies = [ "anyhow", "nom", @@ -6927,9 +5580,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" dependencies = [ "rustls-pki-types", ] @@ -6948,11 +5601,11 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall", "wasite", "web-sys", ] @@ -6975,11 +5628,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6997,6 +5650,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -7015,6 +5698,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -7145,15 +5837,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.50.0" @@ -7165,26 +5848,16 @@ dependencies = [ ] [[package]] -name = "winreg" -version = "0.52.0" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] -name = "x509-cert" -version = "0.2.5" +name = "writeable" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" -dependencies = [ - "const-oid", - "der", - "spki", - "tls_codec", -] +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "x509-certificate" @@ -7207,9 +5880,9 @@ dependencies = [ [[package]] name = "xml-builder" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc4f1a86af7800dfc4056c7833648ea4515ae21502060b5c98114d828f5333b" +checksum = "5ef5f40cd674b9d9814545203f175ac29ffdcb6e006727f4d95797d7badd72e2" [[package]] name = "xml5ever" @@ -7244,18 +5917,32 @@ dependencies = [ [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] -name = "zerocopy" -version = "0.6.6" +name = "yoke" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" dependencies = [ - "byteorder", - "zerocopy-derive 0.6.6", + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "synstructure", ] [[package]] @@ -7264,18 +5951,8 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy-derive" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", + "byteorder", + "zerocopy-derive", ] [[package]] @@ -7286,7 +5963,28 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "synstructure", ] [[package]] @@ -7306,7 +6004,29 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.77", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -7320,18 +6040,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.0" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.12+zstd.1.5.6" +version = "2.0.13+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index dcd389186..807e24e3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.19.6-beta.6" +version = "0.19.6-beta.7" edition = "2021" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -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. @@ -36,16 +37,6 @@ opt-level = "z" # Optimize for size. debug = 0 [features] -embed-pictrs = ["pict-rs"] -# This feature requires building with `tokio_unstable` flag, see documentation: -# https://docs.rs/tokio/latest/tokio/#unstable-features -console = [ - "console-subscriber", - "opentelemetry", - "opentelemetry-otlp", - "tracing-opentelemetry", - "reqwest-tracing/opentelemetry_0_16", -] json-log = ["tracing-subscriber/json"] default = [] @@ -87,20 +78,21 @@ uninlined_format_args = "allow" unused_self = "deny" unwrap_used = "deny" unimplemented = "deny" +unused_async = "deny" [workspace.dependencies] -lemmy_api = { version = "=0.19.6-beta.6", path = "./crates/api" } -lemmy_api_crud = { version = "=0.19.6-beta.6", path = "./crates/api_crud" } -lemmy_apub = { version = "=0.19.6-beta.6", path = "./crates/apub" } -lemmy_utils = { version = "=0.19.6-beta.6", path = "./crates/utils", default-features = false } -lemmy_db_schema = { version = "=0.19.6-beta.6", path = "./crates/db_schema" } -lemmy_api_common = { version = "=0.19.6-beta.6", path = "./crates/api_common" } -lemmy_routes = { version = "=0.19.6-beta.6", path = "./crates/routes" } -lemmy_db_views = { version = "=0.19.6-beta.6", path = "./crates/db_views" } -lemmy_db_views_actor = { version = "=0.19.6-beta.6", path = "./crates/db_views_actor" } -lemmy_db_views_moderator = { version = "=0.19.6-beta.6", path = "./crates/db_views_moderator" } -lemmy_federate = { version = "=0.19.6-beta.6", path = "./crates/federate" } -activitypub_federation = { version = "0.5.8", default-features = false, features = [ +lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" } +lemmy_api_crud = { version = "=0.19.6-beta.7", path = "./crates/api_crud" } +lemmy_apub = { version = "=0.19.6-beta.7", path = "./crates/apub" } +lemmy_utils = { version = "=0.19.6-beta.7", path = "./crates/utils", default-features = false } +lemmy_db_schema = { version = "=0.19.6-beta.7", path = "./crates/db_schema" } +lemmy_api_common = { version = "=0.19.6-beta.7", path = "./crates/api_common" } +lemmy_routes = { version = "=0.19.6-beta.7", path = "./crates/routes" } +lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" } +lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" } +lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" } +lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" } +activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [ "actix-web", ] } diesel = "2.1.6" @@ -108,7 +100,7 @@ diesel_migrations = "2.1.0" diesel-async = "0.4.1" serde = { version = "1.0.204", features = ["derive"] } serde_with = "3.9.0" -actix-web = { version = "4.8.0", default-features = false, features = [ +actix-web = { version = "4.9.0", default-features = false, features = [ "macros", "rustls-0_23", "compress-brotli", @@ -117,19 +109,17 @@ actix-web = { version = "4.8.0", default-features = false, features = [ "cookies", ] } tracing = "0.1.40" -tracing-actix-web = { version = "0.7.11", default-features = false } -tracing-error = "0.2.0" -tracing-log = "0.2.0" +tracing-actix-web = { version = "0.7.10", default-features = false } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } url = { version = "2.5.2", features = ["serde"] } -reqwest = { version = "0.11.27", default-features = false, features = [ +reqwest = { version = "0.12.7", default-features = false, features = [ "json", "blocking", "gzip", "rustls-tls", ] } -reqwest-middleware = "0.2.5" -reqwest-tracing = "0.4.8" +reqwest-middleware = "0.3.3" +reqwest-tracing = "0.5.3" clokwerk = "0.4.0" doku = { version = "0.21.1", features = ["url-2"] } bcrypt = "0.15.1" @@ -143,7 +133,6 @@ anyhow = { version = "1.0.86", features = [ "backtrace", ] } # backtrace is on by default on nightly, but not stable rust diesel_ltree = "0.3.1" -typed-builder = "0.19.1" serial_test = "3.1.1" tokio = { version = "1.39.2", features = ["full"] } regex = "1.10.5" @@ -152,14 +141,13 @@ diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } strum = { version = "0.26.3", features = ["derive"] } itertools = "0.13.0" futures = "0.3.30" -http = "0.2.12" +http = "1.1" rosetta-i18n = "0.1.3" -opentelemetry = { version = "0.19.0", features = ["rt-tokio"] } -tracing-opentelemetry = { version = "0.19.0" } -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" @@ -171,7 +159,9 @@ moka = { version = "0.12.8", features = ["future"] } i-love-jesus = { version = "0.1.0" } clap = { version = "4.5.13", features = ["derive", "env"] } pretty_assertions = "1.4.0" -derive-new = "0.6.0" +derive-new = "0.7.0" +diesel-bind-if-some = "0.1.0" +tuplex = "0.1.2" [dependencies] lemmy_api = { workspace = true } @@ -188,8 +178,6 @@ diesel-async = { workspace = true } actix-web = { workspace = true } tracing = { workspace = true } tracing-actix-web = { workspace = true } -tracing-error = { workspace = true } -tracing-log = { workspace = true } tracing-subscriber = { workspace = true } url = { workspace = true } reqwest = { workspace = true } @@ -197,11 +185,6 @@ reqwest-middleware = { workspace = true } reqwest-tracing = { workspace = true } clokwerk = { workspace = true } serde_json = { workspace = true } -tracing-opentelemetry = { workspace = true, optional = true } -opentelemetry = { workspace = true, optional = true } -console-subscriber = { version = "0.4.0", optional = true } -opentelemetry-otlp = { version = "0.12.0", optional = true } -pict-rs = { version = "0.5.16", optional = true } rustls = { workspace = true } tokio.workspace = true actix-cors = "0.7.0" @@ -210,7 +193,7 @@ chrono = { workspace = true } prometheus = { version = "0.13.4", features = ["process"] } serial_test = { workspace = true } clap = { workspace = true } -actix-web-prom = "0.8.0" +actix-web-prom = "0.9.0" [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/api_tests/package.json b/api_tests/package.json index 4fe5a40bd..9a5057c00 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -6,31 +6,32 @@ "repository": "https://github.com/LemmyNet/lemmy", "author": "Dessalines", "license": "AGPL-3.0", - "packageManager": "pnpm@9.6.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" }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/node": "^22.0.2", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "eslint": "^9.8.0", + "@types/node": "^22.3.0", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", + "eslint": "^9.9.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.19.5-alpha.1", + "lemmy-js-client": "0.20.0-private-community.9", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", - "typescript-eslint": "^8.0.0" + "typescript-eslint": "^8.1.0" } } diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 3da842f5d..b1f18622e 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -10,53 +10,49 @@ importers: devDependencies: '@types/jest': specifier: ^29.5.12 - version: 29.5.12 + version: 29.5.14 '@types/node': - specifier: ^22.0.2 - version: 22.0.2 + specifier: ^22.3.0 + version: 22.8.6 '@typescript-eslint/eslint-plugin': - specifier: ^8.0.0 - version: 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + specifier: ^8.1.0 + 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.0.0 - version: 8.0.0(eslint@9.8.0)(typescript@5.5.4) + specifier: ^8.1.0 + version: 8.12.2(eslint@9.13.0)(typescript@5.6.3) eslint: - specifier: ^9.8.0 - version: 9.8.0 + specifier: ^9.9.0 + version: 9.13.0 eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.2.1(eslint@9.8.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.0.2) + version: 29.7.0(@types/node@22.8.6) lemmy-js-client: - specifier: 0.19.5-alpha.1 - version: 0.19.5-alpha.1 + 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.4(@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.0.2))(typescript@5.5.4) + 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.5.4 + version: 5.6.3 typescript-eslint: - specifier: ^8.0.0 - version: 8.0.0(eslint@9.8.0)(typescript@5.5.4) + specifier: ^8.1.0 + version: 8.12.2(eslint@9.13.0)(typescript@5.6.3) packages: - '@aashutoshrathi/word-wrap@1.2.6': - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - '@ampproject/remapping@2.2.1': resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} - '@babel/code-frame@7.23.5': - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.23.5': @@ -117,6 +113,10 @@ packages: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.23.5': resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} @@ -125,10 +125,6 @@ packages: resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} engines: {node: '>=6.9.0'} - '@babel/highlight@7.23.4': - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} - engines: {node: '>=6.9.0'} - '@babel/parser@7.23.9': resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} engines: {node: '>=6.0.0'} @@ -222,38 +218,54 @@ 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.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + '@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.17.1': - resolution: {integrity: sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==} + '@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.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.8.0': - resolution: {integrity: sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==} + '@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.2': + resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.0': - resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} '@istanbuljs/load-nyc-config@1.1.0': @@ -385,6 +397,9 @@ packages: '@types/babel__traverse@7.20.5': resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -397,11 +412,14 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@29.5.12': - resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - '@types/node@22.0.2': - resolution: {integrity: sha512-yPL6DyFwY5PiMVEwymNeqUTKsDczQBJ/5T7W/46RwLU/VH+AA8aT5TZkvBviLKLbbm0hlfftEkGrNzfRk/fofQ==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.8.6': + resolution: {integrity: sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -412,8 +430,8 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@8.0.0': - resolution: {integrity: sha512-STIZdwEQRXAHvNUS6ILDf5z3u95Gc8jzywunxSNqX00OooIemaaNIA0vEgynJlycL5AjabYLLrIyHd4iazyvtg==} + '@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 @@ -423,8 +441,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.0.0': - resolution: {integrity: sha512-pS1hdZ+vnrpDIxuFXYQpLTILglTjSYJ9MbetZctrUawogUsPdz31DIIRZ9+rab0LhYNTsk88w4fIzVheiTbWOQ==} + '@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 @@ -433,12 +451,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.0.0': - resolution: {integrity: sha512-V0aa9Csx/ZWWv2IPgTfY7T4agYwJyILESu/PVqFtTFz9RIS823mAze+NbnBI8xiwdX3iqeQbcTYlvB04G9wyQw==} + '@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.0.0': - resolution: {integrity: sha512-mJAFP2mZLTBwAn5WI4PMakpywfWFH5nQZezUQdSKV23Pqo6o9iShQg1hP2+0hJJXP2LnZkWPphdIq4juYYwCeg==} + '@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: '*' @@ -446,12 +464,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.0.0': - resolution: {integrity: sha512-wgdSGs9BTMWQ7ooeHtu5quddKKs5Z5dS+fHLbrQI+ID0XWJLODGMHRfhwImiHoeO2S5Wir2yXuadJN6/l4JRxw==} + '@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.0.0': - resolution: {integrity: sha512-5b97WpKMX+Y43YKi4zVcCVLtK5F98dFls3Oxui8LbnmRsseKenbbDinmvxrWegKDMmlkIq/XHuyy0UGLtpCDKg==} + '@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: '*' @@ -459,14 +477,14 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.0.0': - resolution: {integrity: sha512-k/oS/A/3QeGLRvOWCg6/9rATJL5rec7/5s1YmdS0ZU6LHveJyGFwBvLhSRBv6i9xaj7etmosp+l+ViN1I9Aj/Q==} + '@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.0.0': - resolution: {integrity: sha512-oN0K4nkHuOyF3PVMyETbpP5zp6wfyOvm7tWhTMfoqxSSsPmJIh6JNASuZDlODE8eE+0EB9uar+6+vxr9DBTYOA==} + '@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: @@ -474,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 @@ -490,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'} @@ -512,12 +526,8 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} @@ -591,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'} @@ -621,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==} @@ -649,17 +649,8 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.3.6: - resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -690,10 +681,6 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -716,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'} @@ -742,25 +725,30 @@ packages: eslint-config-prettier: optional: true - eslint-scope@8.0.2: - resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + 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.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + 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.8.0: - resolution: {integrity: sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==} + 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: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true - espree@10.1.0: - resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + 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: @@ -768,8 +756,8 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -895,20 +883,12 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 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'} @@ -924,8 +904,8 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} import-fresh@3.3.0: @@ -974,10 +954,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1183,8 +1159,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.19.5-alpha.1: - resolution: {integrity: sha512-GOhaiTQzrpwdmc3DFYemT2SmNmpuQJe2BWUms9QOzdYlkA1WZ0uu7axPE3s+T5OOxfy7K9Q2gsLe72dcVSlffw==} + 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==} @@ -1235,8 +1211,8 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} mimic-fn@2.1.0: @@ -1254,8 +1230,8 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1281,8 +1257,8 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} p-limit@2.3.0: @@ -1328,12 +1304,8 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -1378,8 +1350,8 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -1482,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'} @@ -1520,14 +1488,14 @@ 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' - ts-jest@29.2.4: - resolution: {integrity: sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==} + ts-jest@29.2.5: + resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -1565,8 +1533,8 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript-eslint@8.0.0: - resolution: {integrity: sha512-yQWBJutWL1PmpmDddIOl9/Mi6vZjqNCjqSGBMQ4vsc2Aiodk0SnbQQWPXbSy0HNuKCuGkw1+u4aQ2mO40TdhDQ==} + 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: '*' @@ -1574,13 +1542,13 @@ packages: typescript: optional: true - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.11.1: - resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} update-browserslist-db@1.0.13: resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} @@ -1603,6 +1571,10 @@ packages: engines: {node: '>= 8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1635,24 +1607,23 @@ packages: snapshots: - '@aashutoshrathi/word-wrap@1.2.6': {} - '@ampproject/remapping@2.2.1': dependencies: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.22 - '@babel/code-frame@7.23.5': + '@babel/code-frame@7.26.2': dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 + '@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.23.5 + '@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) @@ -1662,7 +1633,7 @@ snapshots: '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 convert-source-map: 2.0.0 - debug: 4.3.6 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -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,6 +1693,8 @@ snapshots: '@babel/helper-validator-identifier@7.22.20': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-option@7.23.5': {} '@babel/helpers@7.23.9': @@ -1732,12 +1705,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/highlight@7.23.4': - dependencies: - '@babel/helper-validator-identifier': 7.22.20 - chalk: 2.4.2 - js-tokens: 4.0.0 - '@babel/parser@7.23.9': dependencies: '@babel/types': 7.23.9 @@ -1814,13 +1781,13 @@ snapshots: '@babel/template@7.23.9': dependencies: - '@babel/code-frame': 7.23.5 + '@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.23.5 + '@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 @@ -1828,7 +1795,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - debug: 4.3.6 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1841,28 +1808,30 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.0(eslint@9.8.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.13.0)': dependencies: - eslint: 9.8.0 + eslint: 9.13.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.0': {} + '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.17.1': + '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.4 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color + '@eslint/core@0.7.0': {} + '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.4 - espree: 10.1.0 + debug: 4.3.7 + espree: 10.3.0 globals: 14.0.0 - ignore: 5.3.1 + ignore: 5.3.2 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -1870,13 +1839,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.8.0': {} + '@eslint/js@9.13.0': {} '@eslint/object-schema@2.1.4': {} + '@eslint/plugin-kit@0.2.2': + dependencies: + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.0': {} + '@humanwhocodes/retry@0.3.1': {} '@istanbuljs/load-nyc-config@1.1.0': dependencies: @@ -1891,7 +1871,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.0.2 + '@types/node': 22.8.6 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1904,14 +1884,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.0.2 + '@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.0.2) + 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 @@ -1936,7 +1916,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.0.2 + '@types/node': 22.8.6 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1954,7 +1934,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.0.2 + '@types/node': 22.8.6 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1976,7 +1956,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 22.0.2 + '@types/node': 22.8.6 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2046,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.0.2 + '@types/node': 22.8.6 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2112,9 +2092,11 @@ snapshots: dependencies: '@babel/types': 7.23.9 + '@types/estree@1.0.6': {} + '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.0.2 + '@types/node': 22.8.6 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2126,14 +2108,16 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@29.5.12': + '@types/jest@29.5.14': dependencies: expect: 29.7.0 pretty-format: 29.7.0 - '@types/node@22.0.2': + '@types/json-schema@7.0.15': {} + + '@types/node@22.8.6': dependencies: - undici-types: 6.11.1 + undici-types: 6.19.8 '@types/stack-utils@2.0.3': {} @@ -2143,92 +2127,92 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4)': + '@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.0 - '@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.0.0 - '@typescript-eslint/type-utils': 8.0.0(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.0.0(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.0.0 - eslint: 9.8.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.1 + ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4)': + '@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/scope-manager': 8.0.0 - '@typescript-eslint/types': 8.0.0 - '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.0.0 - debug: 4.3.6 - eslint: 9.8.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) + '@typescript-eslint/visitor-keys': 8.12.2 + debug: 4.3.7 + eslint: 9.13.0 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.0.0': + '@typescript-eslint/scope-manager@8.12.2': dependencies: - '@typescript-eslint/types': 8.0.0 - '@typescript-eslint/visitor-keys': 8.0.0 + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/visitor-keys': 8.12.2 - '@typescript-eslint/type-utils@8.0.0(eslint@9.8.0)(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.0.0(eslint@9.8.0)(typescript@5.5.4) - debug: 4.3.6 - ts-api-utils: 1.3.0(typescript@5.5.4) + '@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.4.0(typescript@5.6.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - eslint - supports-color - '@typescript-eslint/types@8.0.0': {} + '@typescript-eslint/types@8.12.2': {} - '@typescript-eslint/typescript-estree@8.0.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.12.2(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.0.0 - '@typescript-eslint/visitor-keys': 8.0.0 - debug: 4.3.6 - globby: 11.1.0 + '@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.5.4) + ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.0.0(eslint@9.8.0)(typescript@5.5.4)': + '@typescript-eslint/utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) - '@typescript-eslint/scope-manager': 8.0.0 - '@typescript-eslint/types': 8.0.0 - '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) - eslint: 9.8.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.0.0': + '@typescript-eslint/visitor-keys@8.12.2': dependencies: - '@typescript-eslint/types': 8.0.0 + '@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: @@ -2243,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 @@ -2264,9 +2244,7 @@ snapshots: argparse@2.0.1: {} - array-union@2.1.0: {} - - async@3.2.5: {} + async@3.2.6: {} babel-jest@29.7.0(@babel/core@7.23.9): dependencies: @@ -2364,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 @@ -2391,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.0.2): + 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.0.2) + jest-config: 29.7.0(@types/node@22.8.6) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2428,13 +2394,9 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.3.4: + debug@4.3.7: dependencies: - ms: 2.1.2 - - debug@4.3.6: - dependencies: - ms: 2.1.2 + ms: 2.1.3 dedent@1.5.1: {} @@ -2446,10 +2408,6 @@ snapshots: diff-sequences@29.6.3: {} - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - ejs@3.1.10: dependencies: jake: 10.9.2 @@ -2466,76 +2424,75 @@ 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.8.0)(prettier@3.3.3): + eslint-plugin-prettier@5.2.1(eslint@9.13.0)(prettier@3.3.3): dependencies: - eslint: 9.8.0 + eslint: 9.13.0 prettier: 3.3.3 prettier-linter-helpers: 1.0.0 synckit: 0.9.1 - eslint-scope@8.0.2: + eslint-scope@8.2.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.0.0: {} + eslint-visitor-keys@4.2.0: {} - eslint@9.8.0: + eslint@9.13.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) - '@eslint-community/regexpp': 4.11.0 - '@eslint/config-array': 0.17.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.7.0 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.8.0 + '@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.0 - '@nodelib/fs.walk': 1.2.8 + '@humanwhocodes/retry': 0.3.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.7 escape-string-regexp: 4.0.0 - eslint-scope: 8.0.2 - eslint-visitor-keys: 4.0.0 - espree: 10.1.0 - esquery: 1.5.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 file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - ignore: 5.3.1 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 - strip-ansi: 6.0.1 + optionator: 0.9.4 text-table: 0.2.0 transitivePeerDependencies: - supports-color - espree@10.1.0: + espree@10.3.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.0.0 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 esprima@4.0.1: {} - esquery@1.5.0: + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -2579,7 +2536,7 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.7 + micromatch: 4.0.8 fast-json-stable-stringify@2.1.0: {} @@ -2662,21 +2619,10 @@ snapshots: globals@14.0.0: {} - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.1 - merge2: 1.4.1 - slash: 3.0.0 - graceful-fs@4.2.11: {} graphemer@1.4.0: {} - has-flag@3.0.0: {} - has-flag@4.0.0: {} hasown@2.0.0: @@ -2687,7 +2633,7 @@ snapshots: human-signals@2.1.0: {} - ignore@5.3.1: {} + ignore@5.3.2: {} import-fresh@3.3.0: dependencies: @@ -2726,8 +2672,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-stream@2.0.1: {} isexe@2.0.0: {} @@ -2762,7 +2706,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.6 + debug: 4.3.7 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -2775,7 +2719,7 @@ snapshots: jake@10.9.2: dependencies: - async: 3.2.5 + async: 3.2.6 chalk: 4.1.2 filelist: 1.0.4 minimatch: 3.1.2 @@ -2792,7 +2736,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.0.2 + '@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.0.2): + 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.0.2) + 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.0.2) + 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.0.2): + 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.0.2 + '@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.0.2 + '@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.0.2 + '@types/node': 22.8.6 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2921,12 +2865,12 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.26.2 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 @@ -2934,7 +2878,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.0.2 + '@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.0.2 + '@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.0.2 + '@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.0.2 + '@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.0.2 + '@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.0.2 + '@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.0.2): + 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.0.2) + 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.19.5-alpha.1: {} + lemmy-js-client@0.20.0-private-community.9: {} leven@3.1.0: {} @@ -3163,7 +3107,7 @@ snapshots: braces: 3.0.2 picomatch: 2.3.1 - micromatch@4.0.7: + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 @@ -3182,7 +3126,7 @@ snapshots: dependencies: brace-expansion: 2.0.1 - ms@2.1.2: {} + ms@2.1.3: {} natural-compare@1.4.0: {} @@ -3204,14 +3148,14 @@ snapshots: dependencies: mimic-fn: 2.1.0 - optionator@0.9.3: + optionator@0.9.4: dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 + word-wrap: 1.2.5 p-limit@2.3.0: dependencies: @@ -3237,7 +3181,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.23.5 + '@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,9 +3194,7 @@ snapshots: path-parse@1.0.7: {} - path-type@4.0.0: {} - - picocolors@1.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3274,7 +3216,7 @@ snapshots: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 18.2.0 + react-is: 18.3.1 prompts@2.4.2: dependencies: @@ -3287,7 +3229,7 @@ snapshots: queue-microtask@1.2.3: {} - react-is@18.2.0: {} + react-is@18.3.1: {} require-directory@2.1.1: {} @@ -3365,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 @@ -3400,22 +3338,22 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@1.3.0(typescript@5.5.4): + ts-api-utils@1.4.0(typescript@5.6.3): dependencies: - typescript: 5.5.4 + typescript: 5.6.3 - ts-jest@29.2.4(@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.0.2))(typescript@5.5.4): + 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.0.2) + 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.5.4 + typescript: 5.6.3 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.23.9 @@ -3433,26 +3371,26 @@ snapshots: type-fest@0.21.3: {} - typescript-eslint@8.0.0(eslint@9.8.0)(typescript@5.5.4): + typescript-eslint@8.12.2(eslint@9.13.0)(typescript@5.6.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.0.0(eslint@9.8.0)(typescript@5.5.4) + '@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.5.4 + typescript: 5.6.3 transitivePeerDependencies: - eslint - supports-color - typescript@5.5.4: {} + typescript@5.6.3: {} - undici-types@6.11.1: {} + undici-types@6.19.8: {} update-browserslist-db@1.0.13(browserslist@4.22.3): dependencies: browserslist: 4.22.3 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.1.1 uri-js@4.4.1: dependencies: @@ -3472,6 +3410,8 @@ snapshots: dependencies: isexe: 2.0.0 + word-wrap@1.2.5: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index 8c3a23ab5..c3f4b3efe 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -92,7 +92,7 @@ test("Create a comment", async () => { test("Create a comment in a non-existent post", async () => { await expect(createComment(alpha, -1)).rejects.toStrictEqual( - Error("couldnt_find_post"), + Error("not_found"), ); }); @@ -143,7 +143,7 @@ test("Delete a comment", async () => { await waitUntil( () => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e), - r => r.message !== "couldnt_find_object", + r => r.message !== "not_found", ) ).comment; if (!gammaComment) { @@ -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 == "couldnt_find_object", + () => 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 === "couldnt_find_object", + () => 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 !== "couldnt_find_object", + () => resolveComment(beta, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === false, ) ).comment; - expect(betaComment2?.comment.deleted).toBe(false); assertCommentFederation(betaComment2, undeleteCommentRes.comment_view); }); @@ -858,3 +857,26 @@ test("Dont send a comment reply to a blocked community", async () => { blockRes = await blockCommunity(beta, newCommunityId, false); expect(blockRes.blocked).toBe(false); }); + +/// Fetching a deeply nested comment can lead to stack overflow as all parent comments are also +/// fetched recursively. Ensure that it works properly. +test.skip("Fetch a deeply nested comment", async () => { + let lastComment; + for (let i = 0; i < 50; i++) { + let commentRes = await createComment( + alpha, + postOnAlphaRes.post_view.post.id, + lastComment?.comment_view.comment.id, + ); + expect(commentRes.comment_view.comment).toBeDefined(); + lastComment = commentRes; + } + + let betaComment = await resolveComment( + beta, + lastComment!.comment_view.comment, + ); + + expect(betaComment!.comment!.comment).toBeDefined(); + expect(betaComment?.comment?.post).toBeDefined(); +}); diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index d8aa6cad6..77b68e2fc 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -527,12 +527,12 @@ test("Content in local-only community doesn't federate", async () => { // cant resolve the community from another instance await expect( resolveCommunity(beta, communityRes.actor_id), - ).rejects.toStrictEqual(Error("couldnt_find_object")); + ).rejects.toStrictEqual(Error("not_found")); // create a post, also cant resolve it let postRes = await createPost(alpha, communityRes.id); await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual( - Error("couldnt_find_object"), + Error("not_found"), ); }); diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 6b5c8d812..59e3d774e 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -125,12 +125,12 @@ test("Create a post", async () => { // Delta only follows beta, so it should not see an alpha ap_id await expect( resolvePost(delta, postRes.post_view.post), - ).rejects.toStrictEqual(Error("couldnt_find_object")); + ).rejects.toStrictEqual(Error("not_found")); // Epsilon has alpha blocked, it should not see the alpha post await expect( resolvePost(epsilon, postRes.post_view.post), - ).rejects.toStrictEqual(Error("couldnt_find_object")); + ).rejects.toStrictEqual(Error("not_found")); // remove added allow/blocklists editSiteForm.allowed_instances = []; @@ -140,9 +140,7 @@ test("Create a post", async () => { }); test("Create a post in a non-existent community", async () => { - await expect(createPost(alpha, -2)).rejects.toStrictEqual( - Error("couldnt_find_community"), - ); + await expect(createPost(alpha, -2)).rejects.toStrictEqual(Error("not_found")); }); test("Unlike a post", async () => { @@ -502,10 +500,17 @@ test("Enforce site ban federation for local user", async () => { alpha, alphaPerson.person.id, false, - false, + true, ); expect(unBanAlpha.banned).toBe(false); + // existing alpha post should be restored on beta + betaBanRes = await waitUntil( + () => getPost(beta, searchBeta1.post.id), + s => !s.post_view.post.removed, + ); + expect(betaBanRes.post_view.post.removed).toBe(false); + // Login gets invalidated by ban, need to login again if (!alphaUserPerson) { throw "Missing alpha person"; @@ -623,7 +628,7 @@ test("Enforce community ban for federated user", async () => { // Alpha tries to make post on beta, but it fails because of ban await expect( createPost(alpha, betaCommunity.community.id), - ).rejects.toStrictEqual(Error("banned_from_community")); + ).rejects.toStrictEqual(Error("person_is_banned_from_community")); // Unban alpha let unBanAlpha = await banPersonFromCommunity( @@ -789,3 +794,29 @@ test("Fetch post with redirect", async () => { let gammaPost2 = await gamma.resolveObject(form); expect(gammaPost2.post).toBeDefined(); }); + +test("Rewrite markdown links", async () => { + const community = (await resolveBetaCommunity(beta)).community!; + + // create a post + let postRes1 = await createPost(beta, community.community.id); + + // link to this post in markdown + let postRes2 = await createPost( + beta, + community.community.id, + "https://example.com/", + `[link](${postRes1.post_view.post.ap_id})`, + ); + console.log(postRes2.post_view.post.body); + expect(postRes2.post_view.post).toBeDefined(); + + // fetch both posts from another instance + const alphaPost1 = await resolvePost(alpha, postRes1.post_view.post); + const alphaPost2 = await resolvePost(alpha, postRes2.post_view.post); + + // remote markdown link is replaced with local link + expect(alphaPost2.post?.post.body).toBe( + `[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`, + ); +}); 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 3ca37dac4..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"); } } @@ -419,13 +426,13 @@ export async function banPersonFromSite( api: LemmyHttp, person_id: number, ban: boolean, - remove_data: boolean, + remove_or_restore_data: boolean, ): Promise { // Make sure lemmy-beta/c/main is cached on lemmy_alpha let form: BanPerson = { person_id, ban, - remove_data, + remove_or_restore_data, }; return api.banPerson(form); } @@ -434,13 +441,13 @@ export async function banPersonFromCommunity( api: LemmyHttp, person_id: number, community_id: number, - remove_data: boolean, + remove_or_restore_data: boolean, ban: boolean, ): Promise { let form: BanFromCommunity = { person_id, community_id, - remove_data: remove_data, + remove_or_restore_data, ban, }; return api.banFromCommunity(form); @@ -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,9 +697,8 @@ export async function saveUserSettingsBio( let form: SaveUserSettings = { show_nsfw: true, blur_nsfw: false, - auto_expand: true, theme: "darkly", - default_sort_type: "Active", + default_post_sort_type: "Active", default_listing_type: "All", interface_language: "en", show_avatars: true, @@ -709,8 +717,7 @@ export async function saveUserSettingsFederated( let form: SaveUserSettings = { show_nsfw: false, blur_nsfw: true, - auto_expand: false, - default_sort_type: "Hot", + default_post_sort_type: "Hot", default_listing_type: "All", interface_language: "", avatar, @@ -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 4bce48b5f..96dc30b79 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -75,6 +75,8 @@ "ProxyAllImages" # Timeout for uploading images to pictrs (in seconds) upload_timeout: 30 + # Resize post thumbnails to this maximum width/height. + 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 0683af9a4..17608a230 100644 --- a/crates/api/src/comment/distinguish.rs +++ b/crates/api/src/comment/distinguish.rs @@ -22,12 +22,11 @@ pub async fn distinguish_comment( data.comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -40,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(), ) @@ -60,8 +59,7 @@ pub async fn distinguish_comment( data.comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; Ok(Json(CommentResponse { comment_view, diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index b8a1c6f76..fbc720102 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ comment::{CommentResponse, CreateCommentLike}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_bot_account, check_community_user_action, check_downvotes_enabled}, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem}, }; use lemmy_db_schema::{ newtypes::LocalUserId, @@ -27,25 +27,30 @@ pub async fn like_comment( local_user_view: LocalUserView, ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; + let comment_id = data.comment_id; let mut recipient_ids = Vec::::new(); - // Don't do a downvote if site has downvotes disabled - check_downvotes_enabled(data.score, &local_site)?; + check_local_vote_mode( + data.score, + VoteItem::Comment(comment_id), + &local_site, + local_user_view.person.id, + &mut context.pool(), + ) + .await?; check_bot_account(&local_user_view.person)?; - let comment_id = data.comment_id; let orig_comment = CommentView::read( &mut context.pool(), comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -54,8 +59,7 @@ pub async fn like_comment( let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await; if let Ok(Some(reply)) = comment_reply { let recipient_id = reply.recipient_id; - if let Ok(Some(local_recipient)) = - LocalUserView::read_person(&mut context.pool(), recipient_id).await + if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await { recipient_ids.push(local_recipient.local_user.id); } @@ -63,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, }; @@ -89,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/list_comment_likes.rs b/crates/api/src/comment/list_comment_likes.rs index 4b2e1c8b3..c9721b8a0 100644 --- a/crates/api/src/comment/list_comment_likes.rs +++ b/crates/api/src/comment/list_comment_likes.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ utils::is_mod_or_admin, }; use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; /// Lists likes for a comment #[tracing::instrument(skip(context))] @@ -19,8 +19,7 @@ pub async fn list_comment_likes( data.comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; is_mod_or_admin( &mut context.pool(), diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs index 67c2db331..6efa6296d 100644 --- a/crates/api/src/comment/save.rs +++ b/crates/api/src/comment/save.rs @@ -37,8 +37,7 @@ pub async fn save_comment( comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; Ok(Json(CommentResponse { comment_view, diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs index a269df07f..48066cfe6 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/comment_report/create.rs @@ -40,12 +40,11 @@ pub async fn create_comment_report( comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; check_community_user_action( &local_user_view.person, - comment_view.community.id, + &comment_view.community, &mut context.pool(), ) .await?; @@ -64,9 +63,8 @@ pub async fn create_comment_report( .await .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; - let comment_report_view = CommentReportView::read(&mut context.pool(), report.id, person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommentReport)?; + let comment_report_view = + CommentReportView::read(&mut context.pool(), report.id, person_id).await?; // Email the admins if local_site.reports_email_admins { @@ -87,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 40aad9569..58d5041dc 100644 --- a/crates/api/src/comment_report/resolve.rs +++ b/crates/api/src/comment_report/resolve.rs @@ -17,14 +17,12 @@ pub async fn resolve_comment_report( ) -> LemmyResult> { let report_id = data.report_id; let person_id = local_user_view.person.id; - let report = CommentReportView::read(&mut context.pool(), report_id, person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommentReport)?; + let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?; 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(), ) @@ -41,9 +39,8 @@ pub async fn resolve_comment_report( } let report_id = data.report_id; - let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommentReport)?; + let comment_report_view = + CommentReportView::read(&mut context.pool(), report_id, person_id).await?; Ok(Json(CommentReportResponse { comment_report_view, diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 8d8826cd2..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,30 +38,23 @@ 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? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; - // 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. if local_user_view.local_user.admin && !community.local { - let is_mod = CommunityModeratorView::is_community_moderator( + CommunityModeratorView::check_is_community_moderator( &mut context.pool(), community.id, local_user_view.person.id, ) .await?; - if !is_mod { - Err(LemmyErrorType::NotAModerator)? - } } // Update in local database @@ -103,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 8e527d2ac..a0e57061b 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -4,11 +4,16 @@ use lemmy_api_common::{ community::{BanFromCommunity, BanFromCommunityResponse}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_community_mod_action, check_expire_time, remove_user_data_in_community}, + utils::{ + check_community_mod_action, + check_expire_time, + remove_or_restore_user_data_in_community, + }, }; use lemmy_db_schema::{ source::{ community::{ + Community, CommunityFollower, CommunityFollowerForm, CommunityPersonBan, @@ -33,13 +38,13 @@ pub async fn ban_from_community( local_user_view: LocalUserView, ) -> LemmyResult> { let banned_person_id = data.person_id; - let remove_data = data.remove_data.unwrap_or(false); 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(), ) @@ -69,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(); @@ -85,9 +85,18 @@ pub async fn ban_from_community( } // Remove/Restore their data if that's desired - if remove_data { - remove_user_data_in_community(data.community_id, banned_person_id, &mut context.pool()).await?; - } + if data.remove_or_restore_data.unwrap_or(false) { + let remove_data = data.ban; + remove_or_restore_user_data_in_community( + data.community_id, + local_user_view.person.id, + banned_person_id, + remove_data, + &data.reason, + &mut context.pool(), + ) + .await?; + }; // Mod tables let form = ModBanFromCommunityForm { @@ -101,9 +110,7 @@ pub async fn ban_from_community( ModBanFromCommunity::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), data.person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let person_view = PersonView::read(&mut context.pool(), data.person_id).await?; ActivityChannel::submit_activity( SendActivityData::BanFromCommunity { @@ -113,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 ad31548ea..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(); @@ -56,8 +51,7 @@ pub async fn block_community( Some(&local_user_view.local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + .await?; ActivityChannel::submit_activity( SendActivityData::FollowCommunity( @@ -66,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 2236fa5bc..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,42 +24,52 @@ pub async fn follow_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let community = Community::read(&mut context.pool(), data.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; - let mut community_follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: local_user_view.person.id, - pending: false, - }; + check_user_valid(&local_user_view.person)?; + let community = Community::read(&mut context.pool(), data.community_id).await?; + 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; @@ -68,8 +79,7 @@ pub async fn follow_community( Some(&local_user_view.local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + .await?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; 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 478192229..121e181c6 100644 --- a/crates/api/src/community/mod.rs +++ b/crates/api/src/community/mod.rs @@ -3,4 +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/random.rs b/crates/api/src/community/random.rs new file mode 100644 index 000000000..3cc04e126 --- /dev/null +++ b/crates/api/src/community/random.rs @@ -0,0 +1,55 @@ +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; +use lemmy_api_common::{ + community::{CommunityResponse, GetRandomCommunity}, + context::LemmyContext, + utils::{check_private_instance, is_mod_or_admin_opt}, +}; +use lemmy_db_schema::source::{ + actor_language::CommunityLanguage, + community::Community, + local_site::LocalSite, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn get_random_community( + data: Query, + context: Data, + local_user_view: Option, +) -> LemmyResult> { + let local_site = LocalSite::read(&mut context.pool()).await?; + + check_private_instance(&local_user_view, &local_site)?; + + let local_user = local_user_view.as_ref().map(|u| &u.local_user); + + let random_community_id = + Community::get_random_community_id(&mut context.pool(), &data.type_).await?; + + let is_mod_or_admin = is_mod_or_admin_opt( + &mut context.pool(), + local_user_view.as_ref(), + Some(random_community_id), + ) + .await + .is_ok(); + + let community_view = CommunityView::read( + &mut context.pool(), + random_community_id, + local_user, + is_mod_or_admin, + ) + .await?; + + let discussion_languages = + CommunityLanguage::read(&mut context.pool(), random_community_id).await?; + + Ok(Json(CommunityResponse { + community_view, + discussion_languages, + })) +} diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index a32c069b1..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()) @@ -82,13 +82,10 @@ pub async fn transfer_community( Some(&local_user_view.local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + .await?; let community_id = data.community_id; - let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; + let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; // Return the jwt Ok(Json(GetCommunityResponse { diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 2b8e12d37..3ab2ba277 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -172,7 +172,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( target: &Person, ban: bool, reason: &Option, - remove_data: &Option, + remove_or_restore_data: &Option, expires: &Option, context: &Data, ) -> LemmyResult<()> { @@ -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 @@ -230,7 +226,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( person_id: target.id, ban, reason: reason.clone(), - remove_data: *remove_data, + remove_or_restore_data: *remove_or_restore_data, expires: *expires, }; @@ -242,8 +238,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( data: ban_from_community, }, context, - ) - .await?; + )?; } } @@ -258,17 +253,13 @@ pub async fn local_user_view_from_jwt( let local_user_id = Claims::validate(jwt, context) .await .with_lemmy_type(LemmyErrorType::NotLoggedIn)?; - let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id) - .await? - .ok_or(LemmyErrorType::CouldntFindLocalUser)?; + let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; check_user_valid(&local_user_view.person)?; Ok(local_user_view) } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs index 44b36fe66..299c9477a 100644 --- a/crates/api/src/local_user/add_admin.rs +++ b/crates/api/src/local_user/add_admin.rs @@ -36,8 +36,8 @@ pub async fn add_admin( // Make sure that the person_id added is local let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id) - .await? - .ok_or(LemmyErrorType::ObjectNotLocal)?; + .await + .map_err(|_| LemmyErrorType::ObjectNotLocal)?; LocalUser::update( &mut context.pool(), diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 58392cefd..9349cc632 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ context::LemmyContext, person::{BanPerson, BanPersonResponse}, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_expire_time, is_admin, remove_user_data}, + utils::{check_expire_time, is_admin, remove_or_restore_user_data}, }; use lemmy_db_schema::{ source::{ @@ -60,15 +60,22 @@ pub async fn ban_from_site( // if its a local user, invalidate logins let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await; - if let Ok(Some(local_user)) = local_user { + if let Ok(local_user) = local_user { LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?; } // Remove their data if that's desired - let remove_data = data.remove_data.unwrap_or(false); - if remove_data { - remove_user_data(person.id, &context).await?; - } + if data.remove_or_restore_data.unwrap_or(false) { + let removed = data.ban; + remove_or_restore_user_data( + local_user_view.person.id, + person.id, + removed, + &data.reason, + &context, + ) + .await?; + }; // Mod tables let form = ModBanForm { @@ -81,16 +88,14 @@ pub async fn ban_from_site( ModBan::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), person.id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let person_view = PersonView::read(&mut context.pool(), person.id).await?; ban_nonlocal_user_from_local_communities( &local_user_view, &person, data.ban, &data.reason, - &data.remove_data, + &data.remove_or_restore_data, &data.expires, &context, ) @@ -101,13 +106,12 @@ pub async fn ban_from_site( moderator: local_user_view.person, banned_user: person_view.person.clone(), reason: data.reason.clone(), - remove_data: data.remove_data, + remove_or_restore_data: data.remove_or_restore_data, ban: data.ban, expires: data.expires, }, &context, - ) - .await?; + )?; Ok(Json(BanPersonResponse { person_view, diff --git a/crates/api/src/local_user/block.rs b/crates/api/src/local_user/block.rs index 698703a9b..250277be3 100644 --- a/crates/api/src/local_user/block.rs +++ b/crates/api/src/local_user/block.rs @@ -32,8 +32,7 @@ pub async fn block_person( let target_user = LocalUserView::read_person(&mut context.pool(), target_id) .await - .ok() - .flatten(); + .ok(); if target_user.is_some_and(|t| t.local_user.admin) { Err(LemmyErrorType::CantBlockAdmin)? @@ -49,9 +48,7 @@ pub async fn block_person( .with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?; } - let person_view = PersonView::read(&mut context.pool(), target_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let person_view = PersonView::read(&mut context.pool(), target_id).await?; Ok(Json(BlockPersonResponse { person_view, blocked: data.block, diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs index 50ee10bb6..03f873a0f 100644 --- a/crates/api/src/local_user/change_password.rs +++ b/crates/api/src/local_user/change_password.rs @@ -28,11 +28,13 @@ pub async fn change_password( } // Check the old password - let valid: bool = verify( - &data.old_password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted + { + verify(&data.old_password, password_encrypted).unwrap_or(false) + } else { + data.old_password.is_empty() + }; + if !valid { Err(LemmyErrorType::IncorrectLogin)? } diff --git a/crates/api/src/local_user/change_password_after_reset.rs b/crates/api/src/local_user/change_password_after_reset.rs index f86421796..191815d0f 100644 --- a/crates/api/src/local_user/change_password_after_reset.rs +++ b/crates/api/src/local_user/change_password_after_reset.rs @@ -21,7 +21,6 @@ pub async fn change_password_after_reset( let token = data.token.clone(); let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token) .await? - .ok_or(LemmyErrorType::TokenNotFound)? .local_user_id; password_length_check(&data.password)?; diff --git a/crates/api/src/local_user/generate_totp_secret.rs b/crates/api/src/local_user/generate_totp_secret.rs index f2bfe7f9c..03ba69759 100644 --- a/crates/api/src/local_user/generate_totp_secret.rs +++ b/crates/api/src/local_user/generate_totp_secret.rs @@ -2,8 +2,11 @@ use crate::{build_totp_2fa, generate_totp_2fa_secret}; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse}; -use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm}; -use lemmy_db_views::structs::{LocalUserView, SiteView}; +use lemmy_db_schema::source::{ + local_user::{LocalUser, LocalUserUpdateForm}, + site::Site, +}; +use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; /// Generate a new secret for two-factor-authentication. Afterwards you need to call [toggle_totp] @@ -13,17 +16,14 @@ pub async fn generate_totp_secret( local_user_view: LocalUserView, context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site = Site::read_local(&mut context.pool()).await?; if local_user_view.local_user.totp_2fa_enabled { return Err(LemmyErrorType::TotpAlreadyEnabled)?; } let secret = generate_totp_2fa_secret(); - let secret_url = - build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url(); + let secret_url = build_totp_2fa(&site.name, &local_user_view.person.name, &secret)?.get_url(); let local_user_form = LocalUserUpdateForm { totp_2fa_secret: Some(Some(secret)), diff --git a/crates/api/src/local_user/list_logins.rs b/crates/api/src/local_user/list_logins.rs index 013236dcd..b5aaf8972 100644 --- a/crates/api/src/local_user/list_logins.rs +++ b/crates/api/src/local_user/list_logins.rs @@ -1,5 +1,5 @@ use actix_web::web::{Data, Json}; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{context::LemmyContext, person::ListLoginsResponse}; use lemmy_db_schema::source::login_token::LoginToken; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; @@ -7,8 +7,8 @@ use lemmy_utils::error::LemmyResult; pub async fn list_logins( context: Data, local_user_view: LocalUserView, -) -> LemmyResult>> { +) -> LemmyResult> { let logins = LoginToken::list(&mut context.pool(), local_user_view.local_user.id).await?; - Ok(Json(logins)) + Ok(Json(ListLoginsResponse { logins })) } diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index 19f84f703..0b2514c5b 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -1,4 +1,4 @@ -use crate::{check_totp_2fa_valid, local_user::check_email_verified}; +use crate::check_totp_2fa_valid; use actix_web::{ web::{Data, Json}, HttpRequest, @@ -8,12 +8,7 @@ use lemmy_api_common::{ claims::Claims, context::LemmyContext, person::{Login, LoginResponse}, - utils::check_user_valid, -}; -use lemmy_db_schema::{ - source::{local_site::LocalSite, registration_application::RegistrationApplication}, - utils::DbPool, - RegistrationMode, + utils::{check_email_verified, check_registration_application, check_user_valid}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -24,23 +19,20 @@ pub async fn login( req: HttpRequest, context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; // Fetch that username / email let username_or_email = data.username_or_email.clone(); let local_user_view = - LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email) - .await? - .ok_or(LemmyErrorType::IncorrectLogin)?; + LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email).await?; // Verify the password - let valid: bool = verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = local_user_view + .local_user + .password_encrypted + .as_ref() + .and_then(|password_encrypted| verify(&data.password, password_encrypted).ok()) + .unwrap_or(false); if !valid { Err(LemmyErrorType::IncorrectLogin)? } @@ -67,28 +59,3 @@ pub async fn login( registration_created: false, })) } - -async fn check_registration_application( - local_user_view: &LocalUserView, - local_site: &LocalSite, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - if (local_site.registration_mode == RegistrationMode::RequireApplication - || local_site.registration_mode == RegistrationMode::Closed) - && !local_user_view.local_user.accepted_application - && !local_user_view.local_user.admin - { - // Fetch the registration application. If no admin id is present its still pending. Otherwise it - // was processed (either accepted or denied). - let local_user_id = local_user_view.local_user.id; - let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id) - .await? - .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; - if registration.admin_id.is_some() { - Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? - } else { - Err(LemmyErrorType::RegistrationApplicationIsPending)? - } - } - Ok(()) -} diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index c00a4516e..b1ee7c0b6 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -1,6 +1,3 @@ -use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; - pub mod add_admin; pub mod ban_person; pub mod block; @@ -20,15 +17,3 @@ pub mod save_settings; pub mod update_totp; pub mod validate_auth; pub mod verify_email; - -/// Check if the user's email is verified if email verification is turned on -/// However, skip checking verification if the user is an admin -fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> { - if !local_user_view.local_user.admin - && site_view.local_site.require_email_verification - && !local_user_view.local_user.email_verified - { - Err(LemmyErrorType::EmailNotVerified)? - } - Ok(()) -} diff --git a/crates/api/src/local_user/notifications/mark_mention_read.rs b/crates/api/src/local_user/notifications/mark_mention_read.rs index 90c8efb6e..9a839b2b4 100644 --- a/crates/api/src/local_user/notifications/mark_mention_read.rs +++ b/crates/api/src/local_user/notifications/mark_mention_read.rs @@ -18,9 +18,7 @@ pub async fn mark_person_mention_as_read( local_user_view: LocalUserView, ) -> LemmyResult> { let person_mention_id = data.person_mention_id; - let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPersonMention)?; + let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?; if local_user_view.person.id != read_person_mention.recipient_id { Err(LemmyErrorType::CouldntUpdateComment)? @@ -39,9 +37,7 @@ pub async fn mark_person_mention_as_read( let person_mention_id = read_person_mention.id; let person_id = local_user_view.person.id; let person_mention_view = - PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)) - .await? - .ok_or(LemmyErrorType::CouldntFindPersonMention)?; + PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?; Ok(Json(PersonMentionResponse { person_mention_view, diff --git a/crates/api/src/local_user/notifications/mark_reply_read.rs b/crates/api/src/local_user/notifications/mark_reply_read.rs index fdcfa5727..5b263145f 100644 --- a/crates/api/src/local_user/notifications/mark_reply_read.rs +++ b/crates/api/src/local_user/notifications/mark_reply_read.rs @@ -18,9 +18,7 @@ pub async fn mark_reply_as_read( local_user_view: LocalUserView, ) -> LemmyResult> { let comment_reply_id = data.comment_reply_id; - let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommentReply)?; + let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?; if local_user_view.person.id != read_comment_reply.recipient_id { Err(LemmyErrorType::CouldntUpdateComment)? @@ -40,9 +38,7 @@ pub async fn mark_reply_as_read( let comment_reply_id = read_comment_reply.id; let person_id = local_user_view.person.id; let comment_reply_view = - CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)) - .await? - .ok_or(LemmyErrorType::CouldntFindCommentReply)?; + CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?; Ok(Json(CommentReplyResponse { comment_reply_view })) } diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs index b6b113c07..5cf06f23a 100644 --- a/crates/api/src/local_user/reset_password.rs +++ b/crates/api/src/local_user/reset_password.rs @@ -1,9 +1,8 @@ -use crate::local_user::check_email_verified; use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, person::PasswordReset, - utils::send_password_reset_email, + utils::{check_email_verified, send_password_reset_email}, SuccessResponse, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; @@ -17,12 +16,10 @@ pub async fn reset_password( // Fetch that email let email = data.email.to_lowercase(); let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email) - .await? - .ok_or(LemmyErrorType::IncorrectLogin)?; + .await + .map_err(|_| LemmyErrorType::IncorrectLogin)?; - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; check_email_verified(&local_user_view, &site_view)?; // Email the pure token to the user. diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index 193f9d269..ac2e321a1 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -36,9 +36,7 @@ pub async fn save_user_settings( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let slur_regex = local_site_to_slur_regex(&site_view.local_site); let url_blocklist = get_url_blocklist(&context).await?; @@ -65,9 +63,7 @@ pub async fn save_user_settings( let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); // if email was changed, check that it is not taken and send verification mail if previous_email.deref() != email { - if LocalUser::is_email_taken(&mut context.pool(), email).await? { - return Err(LemmyErrorType::EmailAlreadyExists)?; - } + LocalUser::check_is_email_taken(&mut context.pool(), email).await?; send_verification_email( &local_user_view, email, @@ -104,7 +100,8 @@ pub async fn save_user_settings( let local_user_id = local_user_view.local_user.id; let person_id = local_user_view.person.id; let default_listing_type = data.default_listing_type; - let default_sort_type = data.default_sort_type; + let default_post_sort_type = data.default_post_sort_type; + let default_comment_sort_type = data.default_comment_sort_type; let person_form = PersonUpdateForm { display_name, @@ -133,10 +130,9 @@ pub async fn save_user_settings( send_notifications_to_email: data.send_notifications_to_email, show_nsfw: data.show_nsfw, blur_nsfw: data.blur_nsfw, - auto_expand: data.auto_expand, show_bot_accounts: data.show_bot_accounts, - show_scores: data.show_scores, - default_sort_type, + default_post_sort_type, + default_comment_sort_type, default_listing_type, theme: data.theme.clone(), interface_language: data.interface_language.clone(), @@ -145,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/local_user/verify_email.rs b/crates/api/src/local_user/verify_email.rs index 5b895ec7e..4b6a8c928 100644 --- a/crates/api/src/local_user/verify_email.rs +++ b/crates/api/src/local_user/verify_email.rs @@ -10,19 +10,15 @@ use lemmy_db_schema::source::{ local_user::{LocalUser, LocalUserUpdateForm}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::error::LemmyResult; pub async fn verify_email( data: Json, context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let token = data.token.clone(); - let verification = EmailVerification::read_for_token(&mut context.pool(), &token) - .await? - .ok_or(LemmyErrorType::TokenNotFound)?; + let verification = EmailVerification::read_for_token(&mut context.pool(), &token).await?; let form = LocalUserUpdateForm { // necessary in case this is a new signup @@ -39,9 +35,7 @@ pub async fn verify_email( // send out notification about registration application to admins if enabled if site_view.local_site.application_email_admins { - let local_user = LocalUserView::read(&mut context.pool(), local_user_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let local_user = LocalUserView::read(&mut context.pool(), local_user_id).await?; send_new_applicant_email_to_admins( &local_user.person.name, diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index ec99a3345..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}, }, @@ -16,7 +17,7 @@ use lemmy_db_schema::{ PostFeatureType, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn feature_post( @@ -25,13 +26,12 @@ pub async fn feature_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + 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(), ) @@ -69,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 e6903fb3c..ec01e3e8c 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -8,19 +8,19 @@ use lemmy_api_common::{ utils::{ check_bot_account, check_community_user_action, - check_downvotes_enabled, + check_local_vote_mode, mark_post_as_read, + VoteItem, }, }; 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; @@ -31,20 +31,24 @@ pub async fn like_post( local_user_view: LocalUserView, ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; + let post_id = data.post_id; - // Don't do a downvote if site has downvotes disabled - check_downvotes_enabled(data.score, &local_site)?; + check_local_vote_mode( + data.score, + VoteItem::Post(post_id), + &local_site, + local_user_view.person.id, + &mut context.pool(), + ) + .await?; check_bot_account(&local_user_view.person)?; // Check for a community ban - let post_id = data.post_id; - let post = Post::read(&mut context.pool(), post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + 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?; @@ -70,20 +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? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; - 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/list_post_likes.rs b/crates/api/src/post/list_post_likes.rs index b9b2106b7..a9b302f2e 100644 --- a/crates/api/src/post/list_post_likes.rs +++ b/crates/api/src/post/list_post_likes.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{source::post::Post, traits::Crud}; use lemmy_db_views::structs::{LocalUserView, VoteView}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; /// Lists likes for a post #[tracing::instrument(skip(context))] @@ -15,9 +15,7 @@ pub async fn list_post_likes( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let post = Post::read(&mut context.pool(), data.post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post = Post::read(&mut context.pool(), data.post_id).await?; is_mod_or_admin( &mut context.pool(), &local_user_view.person, diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index 36f9c2a33..011770c2e 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -14,8 +14,8 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_db_views::structs::{LocalUserView, PostView}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn lock_post( @@ -24,13 +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? - .ok_or(LemmyErrorType::CouldntFindPost)?; + 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(), ) @@ -60,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/save.rs b/crates/api/src/post/save.rs index 85dfc11e3..4549b62b1 100644 --- a/crates/api/src/post/save.rs +++ b/crates/api/src/post/save.rs @@ -40,8 +40,7 @@ pub async fn save_post( Some(&local_user_view.local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?; diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs index 72a40f70d..b9edf35c5 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/post_report/create.rs @@ -35,13 +35,11 @@ pub async fn create_post_report( let person_id = local_user_view.person.id; let post_id = data.post_id; - let post_view = PostView::read(&mut context.pool(), post_id, None, false) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post_view = PostView::read(&mut context.pool(), post_id, None, false).await?; check_community_user_action( &local_user_view.person, - post_view.community.id, + &post_view.community, &mut context.pool(), ) .await?; @@ -61,9 +59,7 @@ pub async fn create_post_report( .await .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; - let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPostReport)?; + let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?; // Email the admins if local_site.reports_email_admins { @@ -84,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 428619674..652327513 100644 --- a/crates/api/src/post_report/resolve.rs +++ b/crates/api/src/post_report/resolve.rs @@ -17,14 +17,12 @@ pub async fn resolve_post_report( ) -> LemmyResult> { let report_id = data.report_id; let person_id = local_user_view.person.id; - let report = PostReportView::read(&mut context.pool(), report_id, person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPostReport)?; + let report = PostReportView::read(&mut context.pool(), report_id, person_id).await?; 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(), ) @@ -40,9 +38,7 @@ pub async fn resolve_post_report( .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; } - let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPostReport)?; + let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id).await?; Ok(Json(PostReportResponse { post_report_view })) } diff --git a/crates/api/src/private_message/mark_read.rs b/crates/api/src/private_message/mark_read.rs index 07e06fe21..7c213464b 100644 --- a/crates/api/src/private_message/mark_read.rs +++ b/crates/api/src/private_message/mark_read.rs @@ -18,9 +18,7 @@ pub async fn mark_pm_as_read( ) -> LemmyResult> { // Checking permissions let private_message_id = data.private_message_id; - let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; + let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; if local_user_view.person.id != orig_private_message.recipient_id { Err(LemmyErrorType::CouldntUpdatePrivateMessage)? } @@ -39,9 +37,7 @@ pub async fn mark_pm_as_read( .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; - let view = PrivateMessageView::read(&mut context.pool(), private_message_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; + let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; Ok(Json(PrivateMessageResponse { private_message_view: view, })) diff --git a/crates/api/src/private_message_report/create.rs b/crates/api/src/private_message_report/create.rs index 41ac592ae..de8ca390f 100644 --- a/crates/api/src/private_message_report/create.rs +++ b/crates/api/src/private_message_report/create.rs @@ -29,9 +29,7 @@ pub async fn create_pm_report( let person_id = local_user_view.person.id; let private_message_id = data.private_message_id; - let private_message = PrivateMessage::read(&mut context.pool(), private_message_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; + let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; // Make sure that only the recipient of the private message can create a report if person_id != private_message.recipient_id { @@ -49,9 +47,8 @@ pub async fn create_pm_report( .await .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; - let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report.id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?; + let private_message_report_view = + PrivateMessageReportView::read(&mut context.pool(), report.id).await?; // Email the admins if local_site.reports_email_admins { diff --git a/crates/api/src/private_message_report/resolve.rs b/crates/api/src/private_message_report/resolve.rs index 27847eeaf..7d821a60c 100644 --- a/crates/api/src/private_message_report/resolve.rs +++ b/crates/api/src/private_message_report/resolve.rs @@ -28,9 +28,8 @@ pub async fn resolve_pm_report( .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; } - let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?; + let private_message_report_view = + PrivateMessageReportView::read(&mut context.pool(), report_id).await?; Ok(Json(PrivateMessageReportResponse { private_message_report_view, diff --git a/crates/api/src/site/federated_instances.rs b/crates/api/src/site/federated_instances.rs index 66b0ff2c1..5943cfd9a 100644 --- a/crates/api/src/site/federated_instances.rs +++ b/crates/api/src/site/federated_instances.rs @@ -5,15 +5,13 @@ use lemmy_api_common::{ utils::build_federated_instances, }; use lemmy_db_views::structs::SiteView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn get_federated_instances( context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let federated_instances = build_federated_instances(&site_view.local_site, &mut context.pool()).await?; diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index e7a5464f3..97ad7e2e5 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -7,11 +7,12 @@ use lemmy_db_schema::{ local_site_url_blocklist::LocalSiteUrlBlocklist, local_user::{LocalUser, LocalUserUpdateForm}, moderator::{ModAdd, ModAddForm}, + oauth_provider::OAuthProvider, tagline::Tagline, }, traits::Crud, }; -use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::structs::PersonView; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, @@ -55,17 +56,14 @@ pub async fn leave_admin( ModAdd::create(&mut context.pool(), &form).await?; // Reread site and admins - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let admins = PersonView::admins(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; - let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; - let custom_emojis = - CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; + let oauth_providers = OAuthProvider::get_all_public(&mut context.pool()).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; + let tagline = Tagline::get_random(&mut context.pool()).await.ok(); Ok(Json(GetSiteResponse { site_view, @@ -74,8 +72,11 @@ pub async fn leave_admin( my_user: None, all_languages, discussion_languages, - taglines, - custom_emojis, + oauth_providers: Some(oauth_providers), + admin_oauth_providers: None, blocked_urls, + tagline, + taglines: vec![], + custom_emojis: vec![], })) } diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index 9f90aff99..ae79a835a 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -16,7 +16,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::{CommentView, LocalUserView}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn purge_comment( @@ -35,8 +35,7 @@ pub async fn purge_comment( comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; // Also check that you're a higher admin LocalUser::is_higher_admin_check( @@ -68,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 59eded6ad..f0252e303 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -19,7 +19,7 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::LocalUserView; use lemmy_db_views_actor::structs::CommunityModeratorView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn purge_community( @@ -31,9 +31,7 @@ pub async fn purge_community( is_admin(&local_user_view)?; // Read the community to get its images - let community = Community::read(&mut context.pool(), data.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community = Community::read(&mut context.pool(), data.community_id).await?; // Also check that you're a higher admin than all the mods let community_mod_person_ids = @@ -77,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 dc824b163..6dad4ce65 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -17,7 +17,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn purge_person( @@ -36,9 +36,7 @@ pub async fn purge_person( ) .await?; - let person = Person::read(&mut context.pool(), data.person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let person = Person::read(&mut context.pool(), data.person_id).await?; ban_nonlocal_user_from_local_communities( &local_user_view, @@ -77,13 +75,12 @@ pub async fn purge_person( moderator: local_user_view.person, banned_user: person, reason: data.reason.clone(), - remove_data: Some(true), + remove_or_restore_data: Some(true), ban: true, 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 6e512312f..f808269e7 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -17,7 +17,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn purge_post( @@ -29,9 +29,7 @@ pub async fn purge_post( is_admin(&local_user_view)?; // Read the post to get the community_id - let post = Post::read(&mut context.pool(), data.post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post = Post::read(&mut context.pool(), data.post_id).await?; // Also check that you're a higher admin LocalUser::is_higher_admin_check( @@ -68,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/approve.rs b/crates/api/src/site/registration_applications/approve.rs index dcde78117..b8cd6c0ea 100644 --- a/crates/api/src/site/registration_applications/approve.rs +++ b/crates/api/src/site/registration_applications/approve.rs @@ -14,10 +14,7 @@ use lemmy_db_schema::{ utils::{diesel_string_update, get_conn}, }; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{LemmyError, LemmyResult}; pub async fn approve_registration_application( data: Json, @@ -61,9 +58,8 @@ pub async fn approve_registration_application( .await?; if data.approve { - let approved_local_user_view = LocalUserView::read(&mut context.pool(), approved_user_id) - .await? - .ok_or(LemmyErrorType::CouldntFindLocalUser)?; + let approved_local_user_view = + LocalUserView::read(&mut context.pool(), approved_user_id).await?; if approved_local_user_view.local_user.email.is_some() { // Email sending may fail, but this won't revert the application approval send_application_approved_email(&approved_local_user_view, context.settings()).await?; @@ -71,9 +67,8 @@ pub async fn approve_registration_application( }; // Read the view - let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id) - .await? - .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; + let registration_application = + RegistrationApplicationView::read(&mut context.pool(), app_id).await?; Ok(Json(RegistrationApplicationResponse { registration_application, diff --git a/crates/api/src/site/registration_applications/get.rs b/crates/api/src/site/registration_applications/get.rs index 2d5d6bf5b..23c6fb4d0 100644 --- a/crates/api/src/site/registration_applications/get.rs +++ b/crates/api/src/site/registration_applications/get.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ utils::is_admin, }; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; /// Lists registration applications, filterable by undenied only. pub async fn get_registration_application( @@ -18,9 +18,7 @@ pub async fn get_registration_application( // Read the view let registration_application = - RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; + RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id).await?; Ok(Json(RegistrationApplicationResponse { registration_application, diff --git a/crates/api/src/site/registration_applications/tests.rs b/crates/api/src/site/registration_applications/tests.rs index 062fa550f..bdfb1535e 100644 --- a/crates/api/src/site/registration_applications/tests.rs +++ b/crates/api/src/site/registration_applications/tests.rs @@ -31,16 +31,16 @@ 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; -#[allow(clippy::unwrap_used)] async fn create_test_site(context: &Data) -> LemmyResult<(Instance, LocalUserView)> { let pool = &mut context.pool(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .expect("Create test instance"); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let admin_person = Person::create( pool, @@ -54,35 +54,26 @@ async fn create_test_site(context: &Data) -> LemmyResult<(Instance ) .await?; - let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id) - .await? - .unwrap(); + let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id).await?; - let site_form = SiteInsertForm::builder() - .name("test site".to_string()) - .instance_id(inserted_instance.id) - .build(); - let site = Site::create(pool, &site_form).await.unwrap(); + let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id); + let site = Site::create(pool, &site_form).await?; // Create a local site, since this is necessary for determining if email verification is // required - let local_site_form = LocalSiteInsertForm::builder() - .site_id(site.id) - .require_email_verification(Some(true)) - .application_question(Some(".".to_string())) - .registration_mode(Some(RegistrationMode::RequireApplication)) - .site_setup(Some(true)) - .build(); - let local_site = LocalSite::create(pool, &local_site_form).await.unwrap(); + let local_site_form = LocalSiteInsertForm { + require_email_verification: Some(true), + application_question: Some(".".to_string()), + registration_mode: Some(RegistrationMode::RequireApplication), + site_setup: Some(true), + ..LocalSiteInsertForm::new(site.id) + }; + let local_site = LocalSite::create(pool, &local_site_form).await?; // Required to have a working local SiteView when updating the site to change email verification // requirement or registration mode - let rate_limit_form = LocalSiteRateLimitInsertForm::builder() - .local_site_id(local_site.id) - .build(); - LocalSiteRateLimit::create(pool, &rate_limit_form) - .await - .unwrap(); + let rate_limit_form = LocalSiteRateLimitInsertForm::new(local_site.id); + LocalSiteRateLimit::create(pool, &rate_limit_form).await?; Ok((inserted_instance, admin_local_user_view)) } @@ -116,7 +107,6 @@ async fn signup( Ok((local_user, application)) } -#[allow(clippy::unwrap_used)] async fn get_application_statuses( context: &Data, admin: LocalUserView, @@ -129,14 +119,14 @@ async fn get_application_statuses( get_unread_registration_application_count(context.reset_request_count(), admin.clone()).await?; let unread_applications = list_registration_applications( - Query::from_query("unread_only=true").unwrap(), + Query::from_query("unread_only=true")?, context.reset_request_count(), admin.clone(), ) .await?; let all_applications = list_registration_applications( - Query::from_query("unread_only=false").unwrap(), + Query::from_query("unread_only=false")?, context.reset_request_count(), admin, ) @@ -145,10 +135,9 @@ async fn get_application_statuses( Ok((application_count, unread_applications, all_applications)) } -#[allow(clippy::indexing_slicing)] -#[allow(clippy::unwrap_used)] -#[tokio::test] #[serial] +#[tokio::test] +#[expect(clippy::indexing_slicing)] async fn test_application_approval() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let pool = &mut context.pool(); diff --git a/crates/api/src/sitemap.rs b/crates/api/src/sitemap.rs index bd0e0dad8..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() @@ -42,44 +40,40 @@ pub async fn get_sitemap(context: Data) -> LemmyResult LemmyResult<()> { let posts: Vec<(DbUrl, DateTime)> = vec![ ( - Url::parse("https://example.com").unwrap().into(), + Url::parse("https://example.com")?.into(), NaiveDate::from_ymd_opt(2022, 12, 1) - .unwrap() + .unwrap_or_default() .and_hms_opt(9, 10, 11) - .unwrap() + .unwrap_or_default() .and_utc(), ), ( - Url::parse("https://lemmy.ml").unwrap().into(), + Url::parse("https://lemmy.ml")?.into(), NaiveDate::from_ymd_opt(2023, 1, 1) - .unwrap() + .unwrap_or_default() .and_hms_opt(1, 2, 3) - .unwrap() + .unwrap_or_default() .and_utc(), ), ]; let mut buf = Vec::::new(); - generate_urlset(posts) - .await - .unwrap() - .write(&mut buf) - .unwrap(); - let root = Element::from_reader(buf.as_slice()).unwrap(); + generate_urlset(posts)?.write(&mut buf)?; + let root = Element::from_reader(buf.as_slice())?; assert_eq!(root.tag().name(), "urlset"); assert_eq!(root.child_count(), 2); @@ -99,45 +93,43 @@ pub(crate) mod tests { root .children() .next() - .unwrap() - .children() - .find(|element| element.tag().name() == "loc") - .unwrap() - .text(), + .and_then(|n| n.children().find(|element| element.tag().name() == "loc")) + .map(Element::text) + .unwrap_or_default(), "https://example.com/" ); assert_eq!( root .children() .next() - .unwrap() - .children() - .find(|element| element.tag().name() == "lastmod") - .unwrap() - .text(), + .and_then(|n| n + .children() + .find(|element| element.tag().name() == "lastmod")) + .map(Element::text) + .unwrap_or_default(), "2022-12-01T09:10:11+00:00" ); assert_eq!( root .children() .nth(1) - .unwrap() - .children() - .find(|element| element.tag().name() == "loc") - .unwrap() - .text(), + .and_then(|n| n.children().find(|element| element.tag().name() == "loc")) + .map(Element::text) + .unwrap_or_default(), "https://lemmy.ml/" ); assert_eq!( root .children() .nth(1) - .unwrap() - .children() - .find(|element| element.tag().name() == "lastmod") - .unwrap() - .text(), + .and_then(|n| n + .children() + .find(|element| element.tag().name() == "lastmod")) + .map(Element::text) + .unwrap_or_default(), "2023-01-01T01:02:03+00:00" ); + + Ok(()) } } 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/build_response.rs b/crates/api_common/src/build_response.rs index 8f140f2fe..d40f4c23d 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -27,7 +27,6 @@ use lemmy_db_views_actor::structs::CommunityView; use lemmy_utils::{ error::LemmyResult, utils::{markdown::markdown_to_html, mention::MentionData}, - LemmyErrorType, }; pub async fn build_comment_response( @@ -37,9 +36,8 @@ pub async fn build_comment_response( recipient_ids: Vec, ) -> LemmyResult { let local_user = local_user_view.map(|l| l.local_user); - let comment_view = CommentView::read(&mut context.pool(), comment_id, local_user.as_ref()) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let comment_view = + CommentView::read(&mut context.pool(), comment_id, local_user.as_ref()).await?; Ok(CommentResponse { comment_view, recipient_ids, @@ -61,8 +59,7 @@ pub async fn build_community_response( Some(&local_user), is_mod_or_admin, ) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + .await?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; Ok(Json(CommunityResponse { @@ -87,8 +84,7 @@ pub async fn build_post_response( Some(&local_user), is_mod_or_admin, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; Ok(Json(PostResponse { post_view })) } @@ -112,8 +108,7 @@ pub async fn send_local_notifs( comment_id, local_user_view.map(|view| &view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; let comment = comment_view.comment; let post = comment_view.post; let community = comment_view.community; @@ -125,7 +120,7 @@ pub async fn send_local_notifs( { let mention_name = mention.name.clone(); let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await; - if let Ok(Some(mention_user_view)) = user_view { + if let Ok(mention_user_view) = user_view { // TODO // At some point, make it so you can't tag the parent creator either // Potential duplication of notifications, one for reply and the other for mention, is handled @@ -161,9 +156,7 @@ pub async fn send_local_notifs( // Send comment_reply to the parent commenter / poster if let Some(parent_comment_id) = comment.parent_comment_id() { - let parent_comment = Comment::read(&mut context.pool(), parent_comment_id) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; // Get the parent commenter local_user let parent_creator_id = parent_comment.creator_id; @@ -182,7 +175,7 @@ pub async fn send_local_notifs( // Don't send a notif to yourself if parent_comment.creator_id != person.id && !check_blocks { let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; - if let Ok(Some(parent_user_view)) = user_view { + if let Ok(parent_user_view) = user_view { // Don't duplicate notif if already mentioned by checking recipient ids if !recipient_ids.contains(&parent_user_view.local_user.id) { recipient_ids.push(parent_user_view.local_user.id); @@ -229,7 +222,7 @@ pub async fn send_local_notifs( if post.creator_id != person.id && !check_blocks { let creator_id = post.creator_id; let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; - if let Ok(Some(parent_user_view)) = parent_user { + if let Ok(parent_user_view) = parent_user { if !recipient_ids.contains(&parent_user_view.local_user.id) { recipient_ids.push(parent_user_view.local_user.id); diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 905394785..759673f4b 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -29,12 +29,8 @@ impl Claims { let claims = decode::(jwt, &key, &validation).with_lemmy_type(LemmyErrorType::NotLoggedIn)?; let user_id = LocalUserId(claims.claims.sub.parse()?); - let is_valid = LoginToken::validate(&mut context.pool(), user_id, jwt).await?; - if !is_valid { - Err(LemmyErrorType::NotLoggedIn)? - } else { - Ok(user_id) - } + LoginToken::validate(&mut context.pool(), user_id, jwt).await?; + Ok(user_id) } pub async fn generate( @@ -73,8 +69,6 @@ impl Claims { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{claims::Claims, context::LemmyContext}; @@ -89,7 +83,7 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; - use lemmy_utils::rate_limit::RateLimitCell; + use lemmy_utils::{error::LemmyResult, rate_limit::RateLimitCell}; use pretty_assertions::assert_eq; use reqwest::Client; use reqwest_middleware::ClientBuilder; @@ -97,10 +91,10 @@ mod tests { #[tokio::test] #[serial] - async fn test_should_not_validate_user_token_after_password_change() { - let pool_ = build_db_pool_for_tests().await; + async fn test_should_not_validate_user_token_after_password_change() -> LemmyResult<()> { + let pool_ = build_db_pool_for_tests(); let pool = &mut (&pool_).into(); - let secret = Secret::init(pool).await.unwrap().unwrap(); + let secret = Secret::init(pool).await?; let context = LemmyContext::create( pool_.clone(), ClientBuilder::new(Client::default()).build(), @@ -108,29 +102,25 @@ mod tests { RateLimitCell::with_test_config(), ); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); - let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) - .await - .unwrap(); + let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; let req = TestRequest::default().to_http_request(); - let jwt = Claims::generate(inserted_local_user.id, req, &context) - .await - .unwrap(); + let jwt = Claims::generate(inserted_local_user.id, req, &context).await?; let valid = Claims::validate(&jwt, &context).await; assert!(valid.is_ok()); - let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, num_deleted); + + Ok(()) } } 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 9d306ff7a..898767b34 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -3,9 +3,14 @@ use lemmy_db_schema::{ source::site::Site, CommunityVisibility, ListingType, - SortType, }; -use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView, PersonView}; +use lemmy_db_views_actor::structs::{ + CommunityModeratorView, + CommunitySortType, + CommunityView, + PendingFollow, + PersonView, +}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -15,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, } @@ -29,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, @@ -44,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, } @@ -73,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, - pub sort: 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, } @@ -97,11 +121,16 @@ pub struct BanFromCommunity { pub community_id: CommunityId, pub person_id: PersonId, pub ban: bool, - pub remove_data: Option, + /// 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, } @@ -140,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, } @@ -163,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, } @@ -184,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, } @@ -223,3 +265,60 @@ pub struct TransferCommunity { pub community_id: CommunityId, pub person_id: PersonId, } + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[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 468d2128d..76bc9c9e2 100644 --- a/crates/api_common/src/custom_emoji.rs +++ b/crates/api_common/src/custom_emoji.rs @@ -1,6 +1,7 @@ use lemmy_db_schema::newtypes::CustomEmojiId; use lemmy_db_views::structs::CustomEmojiView; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; use url::Url; @@ -46,3 +47,27 @@ pub struct DeleteCustomEmoji { pub struct CustomEmojiResponse { pub custom_emoji: CustomEmojiView, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A response for custom emojis. +pub struct ListCustomEmojisResponse { + pub custom_emojis: Vec, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[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 9d12d2e13..6e09d904d 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -7,6 +7,7 @@ pub mod community; #[cfg(feature = "full")] pub mod context; pub mod custom_emoji; +pub mod oauth_provider; pub mod person; pub mod post; pub mod private_message; @@ -15,6 +16,7 @@ pub mod request; #[cfg(feature = "full")] pub mod send_activity; pub mod site; +pub mod tagline; #[cfg(feature = "full")] pub mod utils; @@ -24,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 new file mode 100644 index 000000000..36fef3b18 --- /dev/null +++ b/crates/api_common/src/oauth_provider.rs @@ -0,0 +1,85 @@ +use lemmy_db_schema::newtypes::OAuthProviderId; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; +use url::Url; + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create an external auth method. +pub struct CreateOAuthProvider { + pub display_name: String, + pub issuer: String, + pub authorization_endpoint: String, + pub token_endpoint: String, + pub userinfo_endpoint: String, + pub id_claim: String, + 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, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// 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, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Delete an external auth method. +pub struct DeleteOAuthProvider { + pub id: OAuthProviderId, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Logging in with an OAuth 2.0 authorization +pub struct AuthenticateWithOauth { + pub code: String, + pub oauth_provider_id: OAuthProviderId, + 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 f61f784c2..742dc88db 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -1,11 +1,11 @@ use lemmy_db_schema::{ newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, sensitive::SensitiveString, - source::site::Site, + source::{login_token::LoginToken, site::Site}, CommentSortType, ListingType, PostListingMode, - SortType, + PostSortType, }; use lemmy_db_views::structs::{CommentView, LocalImageView, PostView}; use lemmy_db_views_actor::structs::{ @@ -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,56 +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, - pub auto_expand: Option, /// Your user's theme. + #[cfg_attr(feature = "full", ts(optional))] pub theme: Option, - pub default_sort_type: 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, - /// A post-view mode that changes how multiple post listings look. - pub post_listing_mode: 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, } @@ -154,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, @@ -169,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, - pub sort: 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, } @@ -186,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, @@ -217,12 +270,16 @@ pub struct AddAdminResponse { pub struct BanPerson { pub person_id: PersonId, pub ban: bool, - /// Optionally remove all their data. Useful for new troll accounts. - pub remove_data: Option, + /// 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, } @@ -268,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, } @@ -289,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, } @@ -370,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, } @@ -379,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, } @@ -431,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, } @@ -441,3 +511,10 @@ pub struct ListMedia { pub struct ListMediaResponse { pub images: Vec, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ListLoginsResponse { + pub logins: Vec, +} diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 74369173b..ca4f53e9d 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -2,7 +2,7 @@ use lemmy_db_schema::{ newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId}, ListingType, PostFeatureType, - SortType, + PostSortType, }; use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; @@ -19,17 +19,27 @@ 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, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -43,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, } @@ -68,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, - pub sort: 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, } @@ -94,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, } @@ -114,16 +144,26 @@ 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, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] @@ -143,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, } @@ -226,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, } @@ -267,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, } @@ -276,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, } @@ -289,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 de6ba4f39..96d64d0e5 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -3,7 +3,7 @@ use crate::{ lemmy_db_schema::traits::Crud, post::{LinkMetadata, OpenGraphData}, send_activity::{ActivityChannel, SendActivityData}, - utils::{local_site_opt_to_sensitive, proxy_image_link}, + utils::proxy_image_link, }; use activitypub_federation::config::Data; use chrono::{DateTime, Utc}; @@ -13,8 +13,8 @@ use lemmy_db_schema::{ newtypes::DbUrl, source::{ images::{ImageDetailsForm, LocalImage, LocalImageForm}, - local_site::LocalSite, post::{Post, PostUpdateForm}, + site::Site, }, }; use lemmy_utils::{ @@ -44,6 +44,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder { .user_agent(user_agent.clone()) .timeout(REQWEST_TIMEOUT) .connect_timeout(REQWEST_TIMEOUT) + .use_rustls_tls() } /// Fetches metadata for the given link and optionally generates thumbnail. @@ -130,7 +131,6 @@ pub async fn generate_post_link_metadata( post: Post, custom_thumbnail: Option, send_activity: impl FnOnce(Post) -> Option + Send + 'static, - local_site: Option, context: Data, ) -> LemmyResult<()> { let metadata = match &post.url { @@ -144,7 +144,8 @@ pub async fn generate_post_link_metadata( .is_some_and(|content_type| content_type.starts_with("image")); // Decide if we are allowed to generate local thumbnail - let allow_sensitive = local_site_opt_to_sensitive(&local_site); + let site = Site::read_local(&mut context.pool()).await?; + let allow_sensitive = site.content_warning.is_some(); let allow_generate_thumbnail = allow_sensitive || !post.nsfw; let image_url = if is_image_post { @@ -174,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(()) } @@ -353,9 +354,10 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L // fetch remote non-pictrs images for persistent thumbnail link // TODO: should limit size once supported by pictrs let fetch_url = format!( - "{}image/download?url={}", + "{}image/download?url={}&resize={}", pictrs_config.url, - encode(image_url.as_str()) + encode(image_url.as_str()), + context.settings().pictrs_config()?.max_thumbnail_size ); let res = context @@ -470,14 +472,13 @@ pub async fn replace_image( } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ context::LemmyContext, request::{extract_opengraph_data, fetch_link_metadata}, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; use url::Url; @@ -485,10 +486,10 @@ mod tests { // These helped with testing #[tokio::test] #[serial] - async fn test_link_metadata() { + async fn test_link_metadata() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; - let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap(); - let sample_res = fetch_link_metadata(&sample_url, &context).await.unwrap(); + let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ")?; + let sample_res = fetch_link_metadata(&sample_url, &context).await?; assert_eq!( Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()), sample_res.opengraph_data.title @@ -499,8 +500,7 @@ mod tests { ); assert_eq!( Some( - Url::parse("https://gitlab.com/uploads/-/system/project/avatar/4877469/iod_logo.png") - .unwrap() + Url::parse("https://gitlab.com/uploads/-/system/project/avatar/4877469/iod_logo.png")? .into() ), sample_res.opengraph_data.image @@ -510,19 +510,21 @@ mod tests { Some(mime::TEXT_HTML_UTF_8.to_string()), sample_res.content_type ); + + Ok(()) } #[test] - fn test_resolve_image_url() { + fn test_resolve_image_url() -> LemmyResult<()> { // url that lists the opengraph fields - let url = Url::parse("https://example.com/one/two.html").unwrap(); + let url = Url::parse("https://example.com/one/two.html")?; // root relative url let html_bytes = b""; let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); assert_eq!( metadata.image, - Some(Url::parse("https://example.com/image.jpg").unwrap().into()) + Some(Url::parse("https://example.com/image.jpg")?.into()) ); // base relative url @@ -530,11 +532,7 @@ mod tests { let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); assert_eq!( metadata.image, - Some( - Url::parse("https://example.com/one/image.jpg") - .unwrap() - .into() - ) + Some(Url::parse("https://example.com/one/image.jpg")?.into()) ); // absolute url @@ -542,7 +540,7 @@ mod tests { let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); assert_eq!( metadata.image, - Some(Url::parse("https://cdn.host.com/image.jpg").unwrap().into()) + Some(Url::parse("https://cdn.host.com/image.jpg")?.into()) ); // protocol relative url @@ -550,7 +548,9 @@ mod tests { let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); assert_eq!( metadata.image, - Some(Url::parse("https://example.com/image.jpg").unwrap().into()) + Some(Url::parse("https://example.com/image.jpg")?.into()) ); + + Ok(()) } } diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index 02518ca33..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 { @@ -83,7 +85,7 @@ pub enum SendActivityData { moderator: Person, banned_user: Person, reason: Option, - remove_data: Option, + remove_or_restore_data: Option, ban: bool, expires: Option, }, @@ -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 3850de1c6..40a5cc42d 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -11,34 +11,35 @@ use lemmy_db_schema::{ RegistrationApplicationId, }, source::{ + community::Community, federation_queue_state::FederationQueueState, instance::Instance, language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, + oauth_provider::{OAuthProvider, PublicOAuthProvider}, + person::Person, tagline::Tagline, }, + CommentSortType, + FederationMode, ListingType, ModlogActionType, PostListingMode, + PostSortType, RegistrationMode, SearchType, - SortType, }; use lemmy_db_views::structs::{ CommentView, - CustomEmojiView, LocalUserView, PostView, RegistrationApplicationView, SiteView, }; use lemmy_db_views_actor::structs::{ - CommunityBlockView, CommunityFollowerView, CommunityModeratorView, CommunityView, - InstanceBlockView, - PersonBlockView, PersonView, }; use lemmy_db_views_moderator::structs::{ @@ -70,14 +71,32 @@ 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, - pub sort: 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, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -109,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, } @@ -121,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, } @@ -161,47 +192,96 @@ 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, - pub enable_downvotes: 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, - pub default_sort_type: Option, - pub legal_information: Option, - pub application_email_admins: Option, - pub hide_modlog_mod_names: Option, - pub discussion_languages: Option>, - pub slur_filter_regex: Option, - pub actor_name_max_length: Option, - pub rate_limit_message: Option, - pub rate_limit_message_per_second: Option, - pub rate_limit_post: Option, - pub rate_limit_post_per_second: Option, - pub rate_limit_register: Option, - pub rate_limit_register_per_second: Option, - pub rate_limit_image: Option, - pub rate_limit_image_per_second: Option, - pub rate_limit_comment: Option, - pub rate_limit_comment_per_second: Option, - pub rate_limit_search: Option, - pub rate_limit_search_per_second: Option, - pub federation_enabled: Option, - pub federation_debug: Option, - pub captcha_enabled: Option, - pub captcha_difficulty: Option, - pub allowed_instances: Option>, - pub blocked_instances: Option>, - pub taglines: Option>, - pub registration_mode: Option, - pub content_warning: 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, } #[skip_serializing_none] @@ -210,85 +290,143 @@ 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 downvotes. - pub enable_downvotes: 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, - /// The default sort, usually "active" - pub default_sort_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>, - /// A list of taglines shown at the top of the front page. - pub taglines: 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, - /// Default value for [LocalUser.post_listing_mode] - pub default_post_listing_mode: 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, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -297,7 +435,8 @@ pub struct EditSite { /// The response for a site. pub struct SiteResponse { pub site_view: SiteView, - pub taglines: Vec, + /// deprecated, use field `tagline` or /api/v3/tagline/list + pub taglines: Vec<()>, } #[skip_serializing_none] @@ -309,13 +448,22 @@ 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, - /// A list of taglines shown at the top of the front page. - pub taglines: Vec, - /// A list of custom emojis your site supports. - pub custom_emojis: Vec, + /// deprecated, use field `tagline` or /api/v3/tagline/list + pub taglines: Vec<()>, + /// deprecated, use /api/v3/custom_emoji/list + pub custom_emojis: Vec<()>, + /// If the site has any taglines, a random one is included here for displaying + #[cfg_attr(feature = "full", ts(optional))] + pub tagline: Option, + /// 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, } @@ -326,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, } @@ -337,9 +486,9 @@ pub struct MyUserInfo { pub local_user_view: LocalUserView, pub follows: Vec, pub moderates: Vec, - pub community_blocks: Vec, - pub instance_blocks: Vec, - pub person_blocks: Vec, + pub community_blocks: Vec, + pub instance_blocks: Vec, + pub person_blocks: Vec, pub discussion_languages: Vec, } @@ -361,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>, } @@ -385,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, } @@ -395,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, } @@ -405,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, } @@ -415,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, } @@ -425,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, } @@ -435,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, } @@ -465,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 new file mode 100644 index 000000000..528d37947 --- /dev/null +++ b/crates/api_common/src/tagline.rs @@ -0,0 +1,57 @@ +use lemmy_db_schema::{newtypes::TaglineId, source::tagline::Tagline}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a tagline +pub struct CreateTagline { + pub content: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Update a tagline +pub struct UpdateTagline { + pub id: TaglineId, + pub content: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Delete a tagline +pub struct DeleteTagline { + pub id: TaglineId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct TaglineResponse { + pub tagline: Tagline, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A response for taglines. +pub struct ListTaglinesResponse { + pub taglines: Vec, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[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 0b8e56273..f2c03509d 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -11,32 +11,38 @@ use chrono::{DateTime, Days, Local, TimeZone, Utc}; use enum_map::{enum_map, EnumMap}; use lemmy_db_schema::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, - newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId}, + newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId}, source::{ - comment::{Comment, CommentUpdateForm}, + comment::{Comment, CommentLike, CommentUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm}, community_block::CommunityBlock, email_verification::{EmailVerification, EmailVerificationForm}, - images::RemoteImage, + images::{ImageDetails, RemoteImage}, instance::Instance, instance_block::InstanceBlock, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_site_url_blocklist::LocalSiteUrlBlocklist, + moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm}, + oauth_account::OAuthAccount, password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, person_block::PersonBlock, - post::{Post, PostRead}, + post::{Post, PostLike, PostRead}, + registration_application::RegistrationApplication, site::Site, }, - traits::Crud, + traits::{Crud, Likeable}, utils::DbPool, + FederationMode, + RegistrationMode, }; use lemmy_db_views::{ comment_view::CommentQuery, - structs::{LocalImageView, LocalUserView}, + structs::{LocalImageView, LocalUserView, SiteView}, }; use lemmy_db_views_actor::structs::{ + CommunityFollowerView, CommunityModeratorView, CommunityPersonBanView, CommunityView, @@ -45,10 +51,14 @@ 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::{markdown_check_for_blocked_urls, markdown_rewrite_image_links}, + markdown::{image_links::markdown_rewrite_image_links, markdown_check_for_blocked_urls}, slurs::{build_slur_regex, remove_slurs}, + validation::clean_urls_in_text, }, CACHE_DURATION_FEDERATION, }; @@ -69,13 +79,7 @@ pub async fn is_mod_or_admin( community_id: CommunityId, ) -> LemmyResult<()> { check_user_valid(person)?; - - let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person.id, community_id).await?; - if !is_mod_or_admin { - Err(LemmyErrorType::NotAModOrAdmin)? - } else { - Ok(()) - } + CommunityView::check_is_mod_or_admin(pool, person.id, community_id).await } #[tracing::instrument(skip_all)] @@ -106,13 +110,7 @@ pub async fn check_community_mod_of_any_or_admin_action( let person = &local_user_view.person; check_user_valid(person)?; - - let is_mod_of_any_or_admin = CommunityView::is_mod_of_any_or_admin(pool, person.id).await?; - if !is_mod_of_any_or_admin { - Err(LemmyErrorType::NotAModOrAdmin)? - } else { - Ok(()) - } + CommunityView::check_is_mod_of_any_or_admin(pool, person.id).await } pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> { @@ -168,12 +166,9 @@ pub async fn update_read_comments( person_id, post_id, read_comments, - ..PersonPostAggregatesForm::default() }; - PersonPostAggregates::upsert(pool, &person_post_agg_form) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindPost)?; + PersonPostAggregates::upsert(pool, &person_post_agg_form).await?; Ok(()) } @@ -191,63 +186,85 @@ pub fn check_user_valid(person: &Person) -> LemmyResult<()> { } } +/// Check if the user's email is verified if email verification is turned on +/// However, skip checking verification if the user is an admin +pub fn check_email_verified( + local_user_view: &LocalUserView, + site_view: &SiteView, +) -> LemmyResult<()> { + if !local_user_view.local_user.admin + && site_view.local_site.require_email_verification + && !local_user_view.local_user.email_verified + { + Err(LemmyErrorType::EmailNotVerified)? + } + Ok(()) +} + +pub async fn check_registration_application( + local_user_view: &LocalUserView, + local_site: &LocalSite, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + if (local_site.registration_mode == RegistrationMode::RequireApplication + || local_site.registration_mode == RegistrationMode::Closed) + && !local_user_view.local_user.accepted_application + && !local_user_view.local_user.admin + { + // Fetch the registration application. If no admin id is present its still pending. Otherwise it + // was processed (either accepted or denied). + 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 { + reason: registration.deny_reason, + })? + } else { + Err(LemmyErrorType::RegistrationApplicationIsPending)? + } + } + Ok(()) +} + /// Checks that a normal user action (eg posting or voting) is allowed in a given community. /// /// In particular it checks that neither the user nor community are banned or deleted, and that /// 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?; - check_community_ban(person, community_id, pool).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? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; +pub fn check_community_deleted_removed(community: &Community) -> LemmyResult<()> { if community.deleted || community.removed { Err(LemmyErrorType::Deleted)? } Ok(()) } -async fn check_community_ban( - person: &Person, - community_id: CommunityId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - // check if user was banned from site or community - let is_banned = CommunityPersonBanView::get(pool, person.id, community_id).await?; - if is_banned { - Err(LemmyErrorType::BannedFromCommunity)? - } - Ok(()) -} - /// Check that the given user can perform a mod action in the community. /// /// In particular it checks that he is an admin or mod, wasn't banned and the community isn't /// 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?; - check_community_ban(person, community_id, pool).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(()) } @@ -269,51 +286,6 @@ pub fn check_comment_deleted_or_removed(comment: &Comment) -> LemmyResult<()> { } } -/// Throws an error if a recipient has blocked a person. -#[tracing::instrument(skip_all)] -pub async fn check_person_block( - my_id: PersonId, - potential_blocker_id: PersonId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - let is_blocked = PersonBlock::read(pool, potential_blocker_id, my_id).await?; - if is_blocked { - Err(LemmyErrorType::PersonIsBlocked)? - } else { - Ok(()) - } -} - -/// Throws an error if a recipient has blocked a community. -#[tracing::instrument(skip_all)] -async fn check_community_block( - community_id: CommunityId, - person_id: PersonId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - let is_blocked = CommunityBlock::read(pool, person_id, community_id).await?; - if is_blocked { - Err(LemmyErrorType::CommunityIsBlocked)? - } else { - Ok(()) - } -} - -/// Throws an error if a recipient has blocked an instance. -#[tracing::instrument(skip_all)] -async fn check_instance_block( - instance_id: InstanceId, - person_id: PersonId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - let is_blocked = InstanceBlock::read(pool, person_id, instance_id).await?; - if is_blocked { - Err(LemmyErrorType::InstanceIsBlocked)? - } else { - Ok(()) - } -} - #[tracing::instrument(skip_all)] pub async fn check_person_instance_community_block( my_id: PersonId, @@ -322,19 +294,42 @@ pub async fn check_person_instance_community_block( community_id: CommunityId, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { - check_person_block(my_id, potential_blocker_id, pool).await?; - check_instance_block(community_instance_id, potential_blocker_id, pool).await?; - check_community_block(community_id, potential_blocker_id, pool).await?; + PersonBlock::read(pool, potential_blocker_id, my_id).await?; + InstanceBlock::read(pool, potential_blocker_id, community_instance_id).await?; + CommunityBlock::read(pool, potential_blocker_id, community_id).await?; Ok(()) } +/// A vote item type used to check the vote mode. +pub enum VoteItem { + Post(PostId), + Comment(CommentId), +} + #[tracing::instrument(skip_all)] -pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> LemmyResult<()> { - if score == -1 && !local_site.enable_downvotes { - Err(LemmyErrorType::DownvotesAreDisabled)? - } else { - Ok(()) +pub async fn check_local_vote_mode( + score: i16, + vote_item: VoteItem, + local_site: &LocalSite, + person_id: PersonId, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + let (downvote_setting, upvote_setting) = match vote_item { + VoteItem::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), + VoteItem::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), + }; + + let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable; + let upvote_fail = score == 1 && upvote_setting == FederationMode::Disable; + + // Undo previous vote for item if new vote fails + if downvote_fail || upvote_fail { + match vote_item { + VoteItem::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?, + VoteItem::Comment(comment_id) => CommentLike::remove(pool, person_id, comment_id).await?, + }; } + Ok(()) } /// Dont allow bots to do certain actions, like voting @@ -359,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, @@ -537,13 +542,6 @@ pub fn local_site_opt_to_slur_regex(local_site: &Option) -> Option) -> bool { - local_site - .as_ref() - .map(|site| site.enable_nsfw) - .unwrap_or(false) -} - pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult { static URL_BLOCKLIST: LazyLock> = LazyLock::new(|| { Cache::builder() @@ -667,7 +665,7 @@ pub async fn purge_image_posts_for_person( /// Delete a local_user's images async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { - if let Ok(Some(local_user)) = LocalUserView::read_person(&mut context.pool(), person_id).await { + if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), person_id).await { let pictrs_uploads = LocalImageView::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id) .await?; @@ -706,106 +704,189 @@ pub async fn purge_image_posts_for_community( Ok(()) } -pub async fn remove_user_data( +/// Removes or restores user data. +pub async fn remove_or_restore_user_data( + mod_person_id: PersonId, banned_person_id: PersonId, + removed: bool, + reason: &Option, context: &LemmyContext, ) -> LemmyResult<()> { let pool = &mut context.pool(); - // Purge user images - let person = Person::read(pool, banned_person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; - if let Some(avatar) = person.avatar { - purge_image_from_pictrs(&avatar, context).await.ok(); - } - if let Some(banner) = person.banner { - purge_image_from_pictrs(&banner, context).await.ok(); + + // Only these actions are possible when removing, not restoring + if removed { + // Purge user images + let person = Person::read(pool, banned_person_id).await?; + if let Some(avatar) = person.avatar { + purge_image_from_pictrs(&avatar, context).await.ok(); + } + if let Some(banner) = person.banner { + purge_image_from_pictrs(&banner, context).await.ok(); + } + + // Update the fields to None + Person::update( + pool, + banned_person_id, + &PersonUpdateForm { + avatar: Some(None), + banner: Some(None), + bio: Some(None), + ..Default::default() + }, + ) + .await?; + + // Purge image posts + purge_image_posts_for_person(banned_person_id, context).await?; + + // Communities + // Remove all communities where they're the top mod + // for now, remove the communities manually + let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?; + + // Filter to only this banned users top communities + let banned_user_first_communities: Vec = first_mod_communities + .into_iter() + .filter(|fmc| fmc.moderator.id == banned_person_id) + .collect(); + + for first_mod_community in banned_user_first_communities { + let community_id = first_mod_community.community.id; + Community::update( + pool, + community_id, + &CommunityUpdateForm { + removed: Some(removed), + ..Default::default() + }, + ) + .await?; + + // Delete the community images + if let Some(icon) = first_mod_community.community.icon { + purge_image_from_pictrs(&icon, context).await.ok(); + } + if let Some(banner) = first_mod_community.community.banner { + purge_image_from_pictrs(&banner, context).await.ok(); + } + // Update the fields to None + Community::update( + pool, + community_id, + &CommunityUpdateForm { + icon: Some(None), + banner: Some(None), + ..Default::default() + }, + ) + .await?; + } } - // Update the fields to None - Person::update( + // Posts + let removed_or_restored_posts = + Post::update_removed_for_creator(pool, banned_person_id, None, removed).await?; + create_modlog_entries_for_removed_or_restored_posts( pool, - banned_person_id, - &PersonUpdateForm { - avatar: Some(None), - banner: Some(None), - bio: Some(None), - ..Default::default() - }, + mod_person_id, + removed_or_restored_posts.iter().map(|r| r.id).collect(), + removed, + reason, ) .await?; - // Posts - Post::update_removed_for_creator(pool, banned_person_id, None, true).await?; - - // Purge image posts - purge_image_posts_for_person(banned_person_id, context).await?; - - // Communities - // Remove all communities where they're the top mod - // for now, remove the communities manually - let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?; - - // Filter to only this banned users top communities - let banned_user_first_communities: Vec = first_mod_communities - .into_iter() - .filter(|fmc| fmc.moderator.id == banned_person_id) - .collect(); - - for first_mod_community in banned_user_first_communities { - let community_id = first_mod_community.community.id; - Community::update( - pool, - community_id, - &CommunityUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - - // Delete the community images - if let Some(icon) = first_mod_community.community.icon { - purge_image_from_pictrs(&icon, context).await.ok(); - } - if let Some(banner) = first_mod_community.community.banner { - purge_image_from_pictrs(&banner, context).await.ok(); - } - // Update the fields to None - Community::update( - pool, - community_id, - &CommunityUpdateForm { - icon: Some(None), - banner: Some(None), - ..Default::default() - }, - ) - .await?; - } - // Comments - Comment::update_removed_for_creator(pool, banned_person_id, true).await?; + let removed_or_restored_comments = + Comment::update_removed_for_creator(pool, banned_person_id, removed).await?; + create_modlog_entries_for_removed_or_restored_comments( + pool, + mod_person_id, + removed_or_restored_comments.iter().map(|r| r.id).collect(), + removed, + reason, + ) + .await?; Ok(()) } -pub async fn remove_user_data_in_community( +async fn create_modlog_entries_for_removed_or_restored_posts( + pool: &mut DbPool<'_>, + mod_person_id: PersonId, + post_ids: Vec, + removed: bool, + reason: &Option, +) -> LemmyResult<()> { + // Build the forms + let forms = post_ids + .iter() + .map(|&post_id| ModRemovePostForm { + mod_person_id, + post_id, + removed: Some(removed), + reason: reason.clone(), + }) + .collect(); + + ModRemovePost::create_multiple(pool, &forms).await?; + + Ok(()) +} + +async fn create_modlog_entries_for_removed_or_restored_comments( + pool: &mut DbPool<'_>, + mod_person_id: PersonId, + comment_ids: Vec, + removed: bool, + reason: &Option, +) -> LemmyResult<()> { + // Build the forms + let forms = comment_ids + .iter() + .map(|&comment_id| ModRemoveCommentForm { + mod_person_id, + comment_id, + removed: Some(removed), + reason: reason.clone(), + }) + .collect(); + + ModRemoveComment::create_multiple(pool, &forms).await?; + + Ok(()) +} + +pub async fn remove_or_restore_user_data_in_community( community_id: CommunityId, + mod_person_id: PersonId, banned_person_id: PersonId, + remove: bool, + reason: &Option, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { // Posts - Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?; + let posts = + Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), remove).await?; + create_modlog_entries_for_removed_or_restored_posts( + pool, + mod_person_id, + posts.iter().map(|r| r.id).collect(), + remove, + reason, + ) + .await?; // Comments // TODO Diesel doesn't allow updates with joins, so this has to be a loop + let site = Site::read_local(pool).await?; let comments = CommentQuery { creator_id: Some(banned_person_id), community_id: Some(community_id), ..Default::default() } - .list(pool) + .list(&site, pool) .await?; for comment_view in &comments { @@ -814,22 +895,29 @@ pub async fn remove_user_data_in_community( pool, comment_id, &CommentUpdateForm { - removed: Some(true), + removed: Some(remove), ..Default::default() }, ) .await?; } + create_modlog_entries_for_removed_or_restored_comments( + pool, + mod_person_id, + comments.iter().map(|r| r.comment.id).collect(), + remove, + reason, + ) + .await?; + Ok(()) } pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { let pool = &mut context.pool(); - let person = Person::read(pool, person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let person = Person::read(pool, person_id).await?; // Delete their local images, if they're a local user delete_local_user_images(person_id, context).await.ok(); @@ -858,6 +946,11 @@ pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> // Leave communities they mod CommunityModerator::leave_all_communities(pool, person_id).await?; + // Delete the oauth accounts linked to the local user + if let Ok(local_user) = LocalUserView::read_person(pool, person_id).await { + OAuthAccount::delete_user_accounts(pool, local_user.local_user.id).await?; + } + Person::delete_account(pool, person_id).await?; Ok(()) @@ -892,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()) } @@ -940,6 +1029,18 @@ fn limit_expire_time(expires: DateTime) -> LemmyResult } } +#[tracing::instrument(skip_all)] +pub fn check_conflicting_like_filters( + liked_only: Option, + disliked_only: Option, +) -> LemmyResult<()> { + if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() { + Err(LemmyErrorType::ContradictingFilters)? + } else { + Ok(()) + } +} + pub async fn process_markdown( text: &str, slur_regex: &Option, @@ -947,11 +1048,13 @@ pub async fn process_markdown( context: &LemmyContext, ) -> LemmyResult { let text = remove_slurs(text, slur_regex); + let text = clean_urls_in_text(&text); markdown_check_for_blocked_urls(&text, url_blocklist)?; if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages { let (text, links) = markdown_rewrite_image_links(text); + RemoteImage::create(&mut context.pool(), links.clone()).await?; // Create images and image detail rows for link in links { @@ -961,7 +1064,7 @@ pub async fn process_markdown( let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?; let details_form = details.build_image_details_form(&proxied); - RemoteImage::create(&mut context.pool(), &details_form).await?; + ImageDetails::create(&mut context.pool(), &details_form).await?; } } Ok(text) @@ -997,13 +1100,15 @@ async fn proxy_image_link_internal( if link.domain() == Some(&context.settings().hostname) { Ok(link.into()) } else if image_mode == PictrsImageMode::ProxyAllImages { + RemoteImage::create(&mut context.pool(), vec![link.clone()]).await?; + let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?; // This should fail softly, since pictrs might not even be running let details_res = fetch_pictrs_proxied_image_details(&link, context).await; if let Ok(details) = details_res { let details_form = details.build_image_details_form(&proxied); - RemoteImage::create(&mut context.pool(), &details_form).await?; + ImageDetails::create(&mut context.pool(), &details_form).await?; }; Ok(proxied.into()) @@ -1014,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(), @@ -1071,11 +1176,20 @@ fn build_proxied_image_url( } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; + use lemmy_db_schema::source::{ + comment::CommentInsertForm, + community::CommunityInsertForm, + person::PersonInsertForm, + post::PostInsertForm, + }; + use lemmy_db_views_moderator::structs::{ + ModRemoveCommentView, + ModRemovePostView, + ModlogListParams, + }; use pretty_assertions::assert_eq; use serial_test::serial; @@ -1097,48 +1211,42 @@ mod tests { } #[test] - fn test_limit_ban_term() { + fn test_limit_ban_term() -> LemmyResult<()> { // Ban expires in past, should throw error assert!(limit_expire_time(Utc::now() - Days::new(5)).is_err()); // Legitimate ban term, return same value let fourteen_days = Utc::now() + Days::new(14); - assert_eq!( - limit_expire_time(fourteen_days).unwrap(), - Some(fourteen_days) - ); + assert_eq!(limit_expire_time(fourteen_days)?, Some(fourteen_days)); let nine_years = Utc::now() + Days::new(365 * 9); - assert_eq!(limit_expire_time(nine_years).unwrap(), Some(nine_years)); + assert_eq!(limit_expire_time(nine_years)?, Some(nine_years)); // Too long ban term, changes to None (permanent ban) - assert_eq!( - limit_expire_time(Utc::now() + Days::new(365 * 11)).unwrap(), - None - ); + assert_eq!(limit_expire_time(Utc::now() + Days::new(365 * 11))?, None); + + Ok(()) } #[tokio::test] #[serial] - async fn test_proxy_image_link() { + async fn test_proxy_image_link() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; // image from local domain is unchanged - let local_url = Url::parse("http://lemmy-alpha/image.png").unwrap(); + let local_url = Url::parse("http://lemmy-alpha/image.png")?; let proxied = proxy_image_link_internal(local_url.clone(), PictrsImageMode::ProxyAllImages, &context) - .await - .unwrap(); + .await?; assert_eq!(&local_url, proxied.inner()); // image from remote domain is proxied - let remote_image = Url::parse("http://lemmy-beta/image.png").unwrap(); + let remote_image = Url::parse("http://lemmy-beta/image.png")?; let proxied = proxy_image_link_internal( remote_image.clone(), PictrsImageMode::ProxyAllImages, &context, ) - .await - .unwrap(); + .await?; assert_eq!( "https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png", proxied.as_str() @@ -1149,7 +1257,161 @@ mod tests { assert!( RemoteImage::validate(&mut context.pool(), remote_image.into()) .await - .is_err() + .is_ok() ); + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_mod_remove_or_restore_data() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_mod = PersonInsertForm::test_form(inserted_instance.id, "modder"); + let inserted_mod = Person::create(pool, &new_mod).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "chrimbus"); + let inserted_person = Person::create(pool, &new_person).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "mod_community crepes".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; + + let post_form_1 = PostInsertForm::new( + "A test post tubular".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post_1 = Post::create(pool, &post_form_1).await?; + + let post_form_2 = PostInsertForm::new( + "A test post radical".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post_2 = Post::create(pool, &post_form_2).await?; + + let comment_form_1 = CommentInsertForm::new( + inserted_person.id, + inserted_post_1.id, + "A test comment tubular".into(), + ); + let _inserted_comment_1 = Comment::create(pool, &comment_form_1, None).await?; + + let comment_form_2 = CommentInsertForm::new( + inserted_person.id, + inserted_post_2.id, + "A test comment radical".into(), + ); + let _inserted_comment_2 = Comment::create(pool, &comment_form_2, None).await?; + + // Remove the user data + remove_or_restore_user_data( + inserted_mod.id, + inserted_person.id, + true, + &Some("a remove reason".to_string()), + &context, + ) + .await?; + + // Verify that their posts and comments are removed. + let params = ModlogListParams { + community_id: None, + mod_person_id: None, + other_person_id: None, + post_id: None, + comment_id: None, + page: None, + limit: None, + hide_modlog_names: false, + }; + + // Posts + let post_modlog = ModRemovePostView::list(pool, params).await?; + assert_eq!(2, post_modlog.len()); + + let mod_removed_posts = post_modlog + .iter() + .map(|p| p.mod_remove_post.removed) + .collect::>(); + assert_eq!(vec![true, true], mod_removed_posts); + + let removed_posts = post_modlog + .iter() + .map(|p| p.post.removed) + .collect::>(); + assert_eq!(vec![true, true], removed_posts); + + // Comments + let comment_modlog = ModRemoveCommentView::list(pool, params).await?; + assert_eq!(2, comment_modlog.len()); + + let mod_removed_comments = comment_modlog + .iter() + .map(|p| p.mod_remove_comment.removed) + .collect::>(); + assert_eq!(vec![true, true], mod_removed_comments); + + let removed_comments = comment_modlog + .iter() + .map(|p| p.comment.removed) + .collect::>(); + assert_eq!(vec![true, true], removed_comments); + + // Now restore the content, and make sure it got appended + remove_or_restore_user_data( + inserted_mod.id, + inserted_person.id, + false, + &Some("a restore reason".to_string()), + &context, + ) + .await?; + + // Posts + let post_modlog = ModRemovePostView::list(pool, params).await?; + assert_eq!(4, post_modlog.len()); + + let mod_restored_posts = post_modlog + .iter() + .map(|p| p.mod_remove_post.removed) + .collect::>(); + assert_eq!(vec![false, false, true, true], mod_restored_posts); + + let restored_posts = post_modlog + .iter() + .map(|p| p.post.removed) + .collect::>(); + // All of these will be false, cause its the current state of the post + assert_eq!(vec![false, false, false, false], restored_posts); + + // Comments + let comment_modlog = ModRemoveCommentView::list(pool, params).await?; + assert_eq!(4, comment_modlog.len()); + + let mod_restored_comments = comment_modlog + .iter() + .map(|p| p.mod_remove_comment.removed) + .collect::>(); + assert_eq!(vec![false, false, true, true], mod_restored_comments); + + let restored_comments = comment_modlog + .iter() + .map(|p| p.comment.removed) + .collect::>(); + assert_eq!(vec![false, false, false, false], restored_comments); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index 6055f9ef0..723864705 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -27,8 +27,12 @@ futures.workspace = true uuid = { workspace = true } moka.workspace = true anyhow.workspace = true -webmention = "0.5.0" +chrono.workspace = true +webmention = "0.6.0" accept-language = "3.1.0" +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 49de9d5de..65aa1f612 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -30,10 +30,9 @@ use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, + MAX_COMMENT_DEPTH_LIMIT, }; -const MAX_COMMENT_DEPTH_LIMIT: usize = 100; - #[tracing::instrument(skip(context))] pub async fn create_comment( data: Json, @@ -57,13 +56,17 @@ pub async fn create_comment( Some(&local_user_view.local_user), true, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; 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 @@ -79,8 +82,7 @@ pub async fn create_comment( Comment::read(&mut context.pool(), parent_id).await.ok() } else { None - } - .flatten(); + }; // If there's a parent_id, check to make sure that comment is in that post // Strange issue where sometimes the post ID of the parent comment is incorrect @@ -91,16 +93,9 @@ pub async fn create_comment( check_comment_depth(parent)?; } - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - data.language_id, - community_id, - ) - .await?; - // attempt to set default language if none was provided let language_id = match data.language_id { - Some(lid) => Some(lid), + Some(lid) => lid, None => { default_post_language( &mut context.pool(), @@ -111,12 +106,13 @@ pub async fn create_comment( } }; - let comment_form = CommentInsertForm::builder() - .content(content.clone()) - .post_id(data.post_id) - .creator_id(local_user_view.person.id) - .language_id(language_id) - .build(); + CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id) + .await?; + + let comment_form = CommentInsertForm { + language_id: Some(language_id), + ..CommentInsertForm::new(local_user_view.person.id, data.post_id, content.clone()) + }; // Create the comment let parent_path = parent_opt.clone().map(|t| t.path); @@ -141,7 +137,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, }; @@ -153,8 +148,7 @@ pub async fn create_comment( ActivityChannel::submit_activity( SendActivityData::CreateComment(inserted_comment.clone()), &context, - ) - .await?; + )?; // Update the read comments, so your own new comment doesn't appear as a +1 unread update_read_comments( diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 8c81608c8..60a22a2ef 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -26,8 +26,7 @@ pub async fn delete_comment( comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; // Dont delete it if its already been deleted. if orig_comment.comment.deleted == data.deleted { @@ -36,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?; @@ -77,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 ec4923e93..4e8a1871a 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -31,12 +31,11 @@ pub async fn remove_comment( comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; check_community_mod_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, false, &mut context.pool(), ) @@ -100,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 ed9460825..95cc85fe4 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -41,12 +41,11 @@ pub async fn update_comment( comment_id, Some(&local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -56,13 +55,14 @@ pub async fn update_comment( Err(LemmyErrorType::NoCommentEditAllowed)? } - let language_id = data.language_id; - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - language_id, - orig_comment.community.id, - ) - .await?; + if let Some(language_id) = data.language_id { + CommunityLanguage::is_allowed_community_language( + &mut context.pool(), + language_id, + orig_comment.community.id, + ) + .await?; + } let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; @@ -98,8 +98,7 @@ pub async fn update_comment( ActivityChannel::submit_activity( SendActivityData::UpdateComment(updated_comment.clone()), &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index 4289b7d24..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, + }, }, }; @@ -47,9 +52,7 @@ pub async fn create_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let local_site = site_view.local_site; if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() { @@ -60,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?; @@ -75,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, @@ -90,23 +105,25 @@ pub async fn create_community( // When you create a community, make sure the user becomes a moderator and a follower let keypair = generate_actor_keypair()?; - let community_form = CommunityInsertForm::builder() - .name(data.name.clone()) - .title(data.title.clone()) - .description(description) - .icon(icon) - .banner(banner) - .nsfw(data.nsfw) - .actor_id(Some(community_actor_id.clone())) - .private_key(Some(keypair.private_key)) - .public_key(keypair.public_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())?)) - .posting_restricted_to_mods(data.posting_restricted_to_mods) - .instance_id(site_view.site.instance_id) - .visibility(data.visibility) - .build(); + let community_form = CommunityInsertForm { + sidebar, + description, + icon, + banner, + nsfw: data.nsfw, + 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()?), + posting_restricted_to_mods: data.posting_restricted_to_mods, + visibility: data.visibility, + ..CommunityInsertForm::new( + site_view.site.instance_id, + data.name.clone(), + data.title.clone(), + keypair.public_key, + ) + }; let inserted_community = Community::create(&mut context.pool(), &community_form) .await @@ -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/list.rs b/crates/api_crud/src/community/list.rs index 587b5cdfa..9c13ae89f 100644 --- a/crates/api_crud/src/community/list.rs +++ b/crates/api_crud/src/community/list.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::community_view::CommunityQuery; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn list_communities( @@ -14,9 +14,7 @@ pub async fn list_communities( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let local_site = SiteView::read_local(&mut context.pool()).await?; let is_admin = local_user_view .as_ref() .map(|luv| is_admin(luv).is_ok()) 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 6190a0ca7..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,19 +42,20 @@ 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)?; } - let old_community = Community::read(&mut context.pool(), data.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + 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())?; replace_image(&icon, &old_community.icon, &context).await?; @@ -66,7 +68,7 @@ pub async fn update_community( // Verify its a mod (only mods can edit it) check_community_mod_action( &local_user_view.person, - data.community_id, + &old_community, false, &mut context.pool(), ) @@ -86,6 +88,7 @@ pub async fn update_community( let community_form = CommunityUpdateForm { title: data.title.clone(), + sidebar, description, icon, banner, @@ -104,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/custom_emoji/create.rs b/crates/api_crud/src/custom_emoji/create.rs index 3c5ce3296..333a7ce89 100644 --- a/crates/api_crud/src/custom_emoji/create.rs +++ b/crates/api_crud/src/custom_emoji/create.rs @@ -5,10 +5,12 @@ use lemmy_api_common::{ custom_emoji::{CreateCustomEmoji, CustomEmojiResponse}, utils::is_admin, }; -use lemmy_db_schema::source::{ - custom_emoji::{CustomEmoji, CustomEmojiInsertForm}, - custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, - local_site::LocalSite, +use lemmy_db_schema::{ + source::{ + custom_emoji::{CustomEmoji, CustomEmojiInsertForm}, + custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, + }, + traits::Crud, }; use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; use lemmy_utils::error::LemmyResult; @@ -19,24 +21,20 @@ pub async fn create_custom_emoji( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; // Make sure user is an admin is_admin(&local_user_view)?; - let emoji_form = CustomEmojiInsertForm::builder() - .local_site_id(local_site.id) - .shortcode(data.shortcode.to_lowercase().trim().to_string()) - .alt_text(data.alt_text.to_string()) - .category(data.category.to_string()) - .image_url(data.clone().image_url.into()) - .build(); + let emoji_form = CustomEmojiInsertForm::new( + data.shortcode.to_lowercase().trim().to_string(), + data.clone().image_url.into(), + data.alt_text.to_string(), + data.category.to_string(), + ); let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?; let mut keywords = vec![]; for keyword in &data.keywords { - let keyword_form = CustomEmojiKeywordInsertForm::builder() - .custom_emoji_id(emoji.id) - .keyword(keyword.to_lowercase().trim().to_string()) - .build(); + let keyword_form = + CustomEmojiKeywordInsertForm::new(emoji.id, keyword.to_lowercase().trim().to_string()); keywords.push(keyword_form); } CustomEmojiKeyword::create(&mut context.pool(), keywords).await?; diff --git a/crates/api_crud/src/custom_emoji/delete.rs b/crates/api_crud/src/custom_emoji/delete.rs index 45ac8d0ba..818fd4d88 100644 --- a/crates/api_crud/src/custom_emoji/delete.rs +++ b/crates/api_crud/src/custom_emoji/delete.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ utils::is_admin, SuccessResponse, }; -use lemmy_db_schema::source::custom_emoji::CustomEmoji; +use lemmy_db_schema::{source::custom_emoji::CustomEmoji, traits::Crud}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; diff --git a/crates/api_crud/src/custom_emoji/list.rs b/crates/api_crud/src/custom_emoji/list.rs new file mode 100644 index 000000000..6ee5a44b0 --- /dev/null +++ b/crates/api_crud/src/custom_emoji/list.rs @@ -0,0 +1,25 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + custom_emoji::{ListCustomEmojis, ListCustomEmojisResponse}, +}; +use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn list_custom_emojis( + data: Query, + local_user_view: Option, + context: Data, +) -> Result, LemmyError> { + let custom_emojis = CustomEmojiView::list( + &mut context.pool(), + &data.category, + data.page, + data.limit, + data.ignore_page_limits.unwrap_or(false), + ) + .await?; + + Ok(Json(ListCustomEmojisResponse { custom_emojis })) +} diff --git a/crates/api_crud/src/custom_emoji/mod.rs b/crates/api_crud/src/custom_emoji/mod.rs index fdb2f5561..ffd48daf6 100644 --- a/crates/api_crud/src/custom_emoji/mod.rs +++ b/crates/api_crud/src/custom_emoji/mod.rs @@ -1,3 +1,4 @@ pub mod create; pub mod delete; +pub mod list; pub mod update; diff --git a/crates/api_crud/src/custom_emoji/update.rs b/crates/api_crud/src/custom_emoji/update.rs index 63246f85d..6087f6969 100644 --- a/crates/api_crud/src/custom_emoji/update.rs +++ b/crates/api_crud/src/custom_emoji/update.rs @@ -5,10 +5,12 @@ use lemmy_api_common::{ custom_emoji::{CustomEmojiResponse, EditCustomEmoji}, utils::is_admin, }; -use lemmy_db_schema::source::{ - custom_emoji::{CustomEmoji, CustomEmojiUpdateForm}, - custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, - local_site::LocalSite, +use lemmy_db_schema::{ + source::{ + custom_emoji::{CustomEmoji, CustomEmojiUpdateForm}, + custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, + }, + traits::Crud, }; use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; use lemmy_utils::error::LemmyResult; @@ -19,24 +21,20 @@ pub async fn update_custom_emoji( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; // Make sure user is an admin is_admin(&local_user_view)?; - let emoji_form = CustomEmojiUpdateForm::builder() - .local_site_id(local_site.id) - .alt_text(data.alt_text.to_string()) - .category(data.category.to_string()) - .image_url(data.clone().image_url.into()) - .build(); + let emoji_form = CustomEmojiUpdateForm::new( + data.clone().image_url.into(), + data.alt_text.to_string(), + data.category.to_string(), + ); let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?; CustomEmojiKeyword::delete(&mut context.pool(), data.id).await?; let mut keywords = vec![]; for keyword in &data.keywords { - let keyword_form = CustomEmojiKeywordInsertForm::builder() - .custom_emoji_id(emoji.id) - .keyword(keyword.to_lowercase().trim().to_string()) - .build(); + let keyword_form = + CustomEmojiKeywordInsertForm::new(emoji.id, keyword.to_lowercase().trim().to_string()); keywords.push(keyword_form); } CustomEmojiKeyword::create(&mut context.pool(), keywords).await?; diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs index aee3e8134..7d1b901b9 100644 --- a/crates/api_crud/src/lib.rs +++ b/crates/api_crud/src/lib.rs @@ -1,7 +1,9 @@ pub mod comment; pub mod community; pub mod custom_emoji; +pub mod oauth_provider; pub mod post; pub mod private_message; pub mod site; +pub mod tagline; pub mod user; diff --git a/crates/api_crud/src/oauth_provider/create.rs b/crates/api_crud/src/oauth_provider/create.rs new file mode 100644 index 000000000..fe44ae56e --- /dev/null +++ b/crates/api_crud/src/oauth_provider/create.rs @@ -0,0 +1,42 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + oauth_provider::CreateOAuthProvider, + utils::is_admin, +}; +use lemmy_db_schema::{ + source::oauth_provider::{OAuthProvider, OAuthProviderInsertForm}, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; +use url::Url; + +#[tracing::instrument(skip(context))] +pub async fn create_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let cloned_data = data.clone(); + let oauth_provider_form = OAuthProviderInsertForm { + display_name: cloned_data.display_name, + issuer: Url::parse(&cloned_data.issuer)?.into(), + authorization_endpoint: Url::parse(&cloned_data.authorization_endpoint)?.into(), + token_endpoint: Url::parse(&cloned_data.token_endpoint)?.into(), + userinfo_endpoint: Url::parse(&cloned_data.userinfo_endpoint)?.into(), + id_claim: cloned_data.id_claim, + client_id: data.client_id.to_string(), + client_secret: data.client_secret.to_string(), + scopes: data.scopes.to_string(), + auto_verify_email: data.auto_verify_email, + account_linking_enabled: data.account_linking_enabled, + enabled: data.enabled, + }; + let oauth_provider = OAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?; + Ok(Json(oauth_provider)) +} diff --git a/crates/api_crud/src/oauth_provider/delete.rs b/crates/api_crud/src/oauth_provider/delete.rs new file mode 100644 index 000000000..0d4d616cc --- /dev/null +++ b/crates/api_crud/src/oauth_provider/delete.rs @@ -0,0 +1,25 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + oauth_provider::DeleteOAuthProvider, + utils::is_admin, + SuccessResponse, +}; +use lemmy_db_schema::{source::oauth_provider::OAuthProvider, traits::Crud}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; + +#[tracing::instrument(skip(context))] +pub async fn delete_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + OAuthProvider::delete(&mut context.pool(), data.id) + .await + .with_lemmy_type(LemmyErrorType::CouldntDeleteOauthProvider)?; + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api_crud/src/oauth_provider/mod.rs b/crates/api_crud/src/oauth_provider/mod.rs new file mode 100644 index 000000000..fdb2f5561 --- /dev/null +++ b/crates/api_crud/src/oauth_provider/mod.rs @@ -0,0 +1,3 @@ +pub mod create; +pub mod delete; +pub mod update; diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs new file mode 100644 index 000000000..b4734bf36 --- /dev/null +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -0,0 +1,42 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{context::LemmyContext, oauth_provider::EditOAuthProvider, utils::is_admin}; +use lemmy_db_schema::{ + source::oauth_provider::{OAuthProvider, OAuthProviderUpdateForm}, + traits::Crud, + utils::{diesel_required_string_update, diesel_required_url_update, naive_now}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn update_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let cloned_data = data.clone(); + let oauth_provider_form = OAuthProviderUpdateForm { + display_name: diesel_required_string_update(cloned_data.display_name.as_deref()), + authorization_endpoint: diesel_required_url_update( + cloned_data.authorization_endpoint.as_deref(), + )?, + token_endpoint: diesel_required_url_update(cloned_data.token_endpoint.as_deref())?, + userinfo_endpoint: diesel_required_url_update(cloned_data.userinfo_endpoint.as_deref())?, + id_claim: diesel_required_string_update(data.id_claim.as_deref()), + client_secret: diesel_required_string_update(data.client_secret.as_deref()), + scopes: diesel_required_string_update(data.scopes.as_deref()), + auto_verify_email: data.auto_verify_email, + account_linking_enabled: data.account_linking_enabled, + enabled: data.enabled, + updated: Some(Some(naive_now())), + }; + + let update_result = + OAuthProvider::update(&mut context.pool(), data.id, &oauth_provider_form).await?; + let oauth_provider = OAuthProvider::read(&mut context.pool(), update_result.id).await?; + Ok(Json(oauth_provider)) +} diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index a0f0b7525..16932cacb 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -1,3 +1,4 @@ +use super::convert_published_time; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ @@ -84,72 +85,68 @@ 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? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; if community.posting_restricted_to_mods { let community_id = data.community_id; - let is_mod = CommunityModeratorView::is_community_moderator( + CommunityModeratorView::check_is_community_moderator( &mut context.pool(), community_id, local_user_view.local_user.person_id, ) .await?; - if !is_mod { - Err(LemmyErrorType::OnlyModsCanPostInCommunity)? - } } - // Only need to check if language is allowed in case user set it explicitly. When using default - // language, it already only returns allowed languages. - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - data.language_id, - community_id, - ) - .await?; - // attempt to set default language if none was provided let language_id = match data.language_id { - Some(lid) => Some(lid), + Some(lid) => lid, None => { default_post_language( &mut context.pool(), - community_id, + community.id, local_user_view.local_user.id, ) .await? } }; - let post_form = PostInsertForm::builder() - .name(data.name.trim().to_string()) - .url(url.map(Into::into)) - .body(body) - .alt_text(data.alt_text.clone()) - .community_id(data.community_id) - .creator_id(local_user_view.person.id) - .nsfw(data.nsfw) - .language_id(language_id) - .build(); + // Only need to check if language is allowed in case user set it explicitly. When using default + // language, it already only returns allowed languages. + CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community.id) + .await?; + + let scheduled_publish_time = + convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?; + let post_form = PostInsertForm { + url: url.map(Into::into), + body, + alt_text: data.alt_text.clone(), + nsfw: data.nsfw, + language_id: Some(language_id), + scheduled_publish_time, + ..PostInsertForm::new( + data.name.trim().to_string(), + local_user_view.person.id, + data.community_id, + ) + }; let inserted_post = Post::create(&mut context.pool(), &post_form) .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)) + } else { + |_| None + }; generate_post_link_metadata( inserted_post.clone(), custom_thumbnail.map(Into::into), - |post| Some(SendActivityData::CreatePost(post)), - Some(local_site), + federate_post, context.reset_request_count(), ) .await?; @@ -169,11 +166,14 @@ pub async fn create_post( mark_post_as_read(person_id, post_id, &mut context.pool()).await?; - if let Some(url) = inserted_post.url.clone() { + build_post_response(&context, community_id, local_user_view, post_id).await +} + +pub fn send_webmention(post: Post, community: Community) { + if let Some(url) = post.url.clone() { if community.visibility == CommunityVisibility::Public { spawn_try_task(async move { - let mut webmention = - Webmention::new::(inserted_post.ap_id.clone().into(), url.clone().into())?; + let mut webmention = Webmention::new::(post.ap_id.clone().into(), url.clone().into())?; webmention.set_checked(true); match webmention .send() @@ -187,6 +187,4 @@ pub async fn create_post( }); } }; - - build_post_response(&context, community_id, local_user_view, post_id).await } diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs index 6834030ac..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; @@ -21,21 +24,15 @@ pub async fn delete_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let orig_post = Post::read(&mut context.pool(), post_id).await?; // Dont delete it if its already been deleted. if orig_post.deleted == data.deleted { 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) { @@ -56,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 8bb842b70..5db0ad5d0 100644 --- a/crates/api_crud/src/post/mod.rs +++ b/crates/api_crud/src/post/mod.rs @@ -1,5 +1,38 @@ +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::{LemmyErrorType, LemmyResult}; + pub mod create; pub mod delete; pub mod read; pub mod remove; pub mod update; + +async fn convert_published_time( + scheduled_publish_time: Option, + local_user_view: &LocalUserView, + context: &LemmyContext, +) -> LemmyResult>> { + const MAX_SCHEDULED_POSTS: i64 = 10; + if let Some(scheduled_publish_time) = scheduled_publish_time { + let converted = Utc + .timestamp_opt(scheduled_publish_time, 0) + .single() + .ok_or(LemmyErrorType::InvalidUnixTime)?; + if converted < Utc::now() { + Err(LemmyErrorType::PostScheduleTimeMustBeInFuture)?; + } + if !local_user_view.local_user.admin { + let count = + Post::user_scheduled_post_count(local_user_view.person.id, &mut context.pool()).await?; + if count >= MAX_SCHEDULED_POSTS { + Err(LemmyErrorType::TooManyScheduledPosts)?; + } + } + Ok(Some(converted)) + } else { + Ok(None) + } +} diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index ebf8940a2..7677d59ef 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -21,9 +21,7 @@ pub async fn get_post( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let local_site = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site.local_site)?; @@ -35,16 +33,14 @@ pub async fn get_post( } else if let Some(comment_id) = data.comment_id { Comment::read(&mut context.pool(), comment_id) .await? - .ok_or(LemmyErrorType::CouldntFindComment)? .post_id } else { - Err(LemmyErrorType::CouldntFindPost)? + Err(LemmyErrorType::NotFound)? }; // Check to see if the person is a mod or admin, to show deleted / removed - let community_id = Post::read(&mut context.pool(), post_id) + let community_id = Post::read_xx(&mut context.pool(), post_id) .await? - .ok_or(LemmyErrorType::CouldntFindPost)? .community_id; let is_mod_or_admin = is_mod_or_admin_opt( @@ -62,8 +58,7 @@ pub async fn get_post( local_user.as_ref(), is_mod_or_admin, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; let post_id = post_view.post.id; if let Some(person_id) = person_id { @@ -85,15 +80,15 @@ pub async fn get_post( local_user.as_ref(), is_mod_or_admin, ) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + .await?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; // Fetch the cross_posts let cross_posts = if let Some(url) = &post_view.post.url { let mut x_posts = PostQuery { - url_search: Some(url.inner().as_str().into()), + url_only: Some(true), + search_term: Some(url.inner().as_str().into()), local_user: local_user.as_ref(), ..Default::default() } diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index b4fdba6fb..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}, @@ -17,7 +18,7 @@ use lemmy_db_schema::{ traits::{Crud, Reportable}, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn remove_post( @@ -26,13 +27,16 @@ pub async fn remove_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + + // 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(), ) @@ -79,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 835398596..fc23e7d9e 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -1,3 +1,4 @@ +use super::{convert_published_time, create::send_webmention}; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ @@ -16,13 +17,14 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ actor_language::CommunityLanguage, + community::Community, local_site::LocalSite, post::{Post, PostUpdateForm}, }, 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::{ @@ -85,29 +87,43 @@ pub async fn update_post( } let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + 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 = data.language_id; - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - language_id, - orig_post.community_id, - ) - .await?; + if let Some(language_id) = data.language_id { + CommunityLanguage::is_allowed_community_language( + &mut context.pool(), + language_id, + orig_post.community.id, + ) + .await?; + } + + // handle changes to scheduled_publish_time + let scheduled_publish_time = match ( + orig_post.post.scheduled_publish_time, + data.scheduled_publish_time, + ) { + // schedule time can be changed if post is still scheduled (and not published yet) + (Some(_), Some(_)) => { + Some(convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?) + } + // post was scheduled, gets changed to publish immediately + (Some(_), None) => Some(None), + // unchanged + (_, _) => None, + }; let post_form = PostUpdateForm { name: data.name.clone(), @@ -117,6 +133,7 @@ pub async fn update_post( nsfw: data.nsfw, language_id: data.language_id, updated: Some(Some(naive_now())), + scheduled_publish_time, ..Default::default() }; @@ -125,18 +142,40 @@ pub async fn update_post( .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; - generate_post_link_metadata( - updated_post.clone(), - custom_thumbnail.flatten().map(Into::into), - |post| Some(SendActivityData::UpdatePost(post)), - Some(local_site), - context.reset_request_count(), - ) - .await?; + // send out federation/webmention if necessary + match ( + 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?; + send_webmention(updated_post.clone(), community); + generate_post_link_metadata( + updated_post.clone(), + custom_thumbnail.flatten().map(Into::into), + |post| Some(SendActivityData::CreatePost(post)), + context.reset_request_count(), + ) + .await?; + } + // post was already public, send update + (None, _) => { + generate_post_link_metadata( + updated_post.clone(), + custom_thumbnail.flatten().map(Into::into), + |post| Some(SendActivityData::UpdatePost(post)), + context.reset_request_count(), + ) + .await? + } + // schedule was changed, do nothing + (Some(_), Some(_)) => {} + }; 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 46908da6e..1a6a78d00 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ private_message::{CreatePrivateMessage, PrivateMessageResponse}, send_activity::{ActivityChannel, SendActivityData}, utils::{ - check_person_block, + check_private_messages_enabled, get_interface_language, get_url_blocklist, local_site_to_slur_regex, @@ -16,6 +16,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_site::LocalSite, + person_block::PersonBlock, private_message::{PrivateMessage, PrivateMessageInsertForm}, }, traits::Crud, @@ -39,33 +40,39 @@ pub async fn create_private_message( let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; is_valid_body_field(&content, false)?; - check_person_block( - local_user_view.person.id, - data.recipient_id, + PersonBlock::read( &mut context.pool(), + data.recipient_id, + local_user_view.person.id, ) .await?; - let private_message_form = PrivateMessageInsertForm::builder() - .content(content.clone()) - .creator_id(local_user_view.person.id) - .recipient_id(data.recipient_id) - .build(); + 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, + content.clone(), + ); let inserted_private_message = PrivateMessage::create(&mut context.pool(), &private_message_form) .await .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?; - let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; + let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?; // Send email to the local recipient, if one exists if view.recipient.local { let recipient_id = data.recipient_id; - let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?; let lang = get_interface_language(&local_recipient); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let sender_name = &local_user_view.person.name; @@ -82,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 dc028ff41..30efc020c 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -20,9 +20,7 @@ pub async fn delete_private_message( ) -> LemmyResult> { // Checking permissions let private_message_id = data.private_message_id; - let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; + let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; if local_user_view.person.id != orig_private_message.creator_id { Err(LemmyErrorType::EditPrivateMessageNotAllowed)? } @@ -44,12 +42,9 @@ 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_or(LemmyErrorType::CouldntFindPrivateMessage)?; + let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; Ok(Json(PrivateMessageResponse { private_message_view: view, })) diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 364d5c2e3..aa562c626 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -30,9 +30,7 @@ pub async fn update_private_message( // Checking permissions let private_message_id = data.private_message_id; - let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; + let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; if local_user_view.person.id != orig_private_message.creator_id { Err(LemmyErrorType::EditPrivateMessageNotAllowed)? } @@ -56,15 +54,12 @@ pub async fn update_private_message( .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; - let view = PrivateMessageView::read(&mut context.pool(), private_message_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; + let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; 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 9dcd1595a..e1ea1d992 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -1,3 +1,4 @@ +use super::not_zero; use crate::site::{application_question_check, site_default_post_listing_type_check}; use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair}; use actix_web::web::Json; @@ -5,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, @@ -20,7 +21,6 @@ use lemmy_db_schema::{ local_site::{LocalSite, LocalSiteUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, site::{Site, SiteUpdateForm}, - tagline::Tagline, }, traits::Crud, utils::{diesel_string_update, diesel_url_create, naive_now}, @@ -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); @@ -90,16 +90,15 @@ pub async fn create_site( let local_site_form = LocalSiteUpdateForm { // Set the site setup to true site_setup: Some(true), - enable_downvotes: data.enable_downvotes, registration_mode: data.registration_mode, - enable_nsfw: data.enable_nsfw, community_creation_admin_only: data.community_creation_admin_only, require_email_verification: data.require_email_verification, application_question: diesel_string_update(data.application_question.as_deref()), private_instance: data.private_instance, default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, - default_sort_type: data.default_sort_type, + default_post_sort_type: data.default_post_sort_type, + default_comment_sort_type: data.default_comment_sort_type, legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, @@ -110,6 +109,10 @@ pub async fn create_site( captcha_enabled: data.captcha_enabled, captcha_difficulty: data.captcha_difficulty.clone(), default_post_listing_mode: data.default_post_listing_mode, + post_upvotes: data.post_upvotes, + post_downvotes: data.post_downvotes, + comment_upvotes: data.comment_upvotes, + comment_downvotes: data.comment_downvotes, ..Default::default() }; @@ -117,28 +120,23 @@ pub async fn create_site( let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm { message: data.rate_limit_message, - message_per_second: data.rate_limit_message_per_second, + message_per_second: not_zero(data.rate_limit_message_per_second), post: data.rate_limit_post, - post_per_second: data.rate_limit_post_per_second, + post_per_second: not_zero(data.rate_limit_post_per_second), register: data.rate_limit_register, - register_per_second: data.rate_limit_register_per_second, + register_per_second: not_zero(data.rate_limit_register_per_second), image: data.rate_limit_image, - image_per_second: data.rate_limit_image_per_second, + image_per_second: not_zero(data.rate_limit_image_per_second), comment: data.rate_limit_comment, - comment_per_second: data.rate_limit_comment_per_second, + comment_per_second: not_zero(data.rate_limit_comment_per_second), search: data.rate_limit_search, - search_per_second: data.rate_limit_search_per_second, + search_per_second: not_zero(data.rate_limit_search_per_second), ..Default::default() }; LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?; - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; - - let new_taglines = data.taglines.clone(); - let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let rate_limit_config = local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); @@ -146,7 +144,7 @@ pub async fn create_site( Ok(Json(SiteResponse { site_view, - taglines, + taglines: vec![], })) } @@ -169,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)?; @@ -197,13 +195,16 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::site::create::validate_create_payload; use lemmy_api_common::site::CreateSite; - use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode, SortType}; + use lemmy_db_schema::{ + source::local_site::LocalSite, + ListingType, + PostSortType, + RegistrationMode, + }; use lemmy_utils::error::LemmyErrorType; #[test] @@ -212,170 +213,114 @@ mod tests { ( "CreateSite attempted on set up LocalSite", LemmyErrorType::SiteAlreadyExists, - &generate_local_site( - true, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: true, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + ..Default::default() + }, ), ( "CreateSite name matches LocalSite slur filter", LemmyErrorType::Slurs, - &generate_local_site( - false, - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("foo site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("foo site_name"), + ..Default::default() + }, ), ( "CreateSite name matches new slur filter", LemmyErrorType::Slurs, - &generate_local_site( - false, - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("zeta site_name"), - None::, - None::, - None::, - None::, - Some(String::from("(zeta|alpha)")), - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("zeta site_name"), + slur_filter_regex: Some(String::from("(zeta|alpha)")), + ..Default::default() + }, ), ( "CreateSite listing type is Subscribed, which is invalid", LemmyErrorType::InvalidDefaultPostListingType, - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - Some(ListingType::Subscribed), - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + default_post_listing_type: Some(ListingType::Subscribed), + ..Default::default() + }, ), ( "CreateSite is both private and federated", LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - Some(true), - Some(true), - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + private_instance: Some(true), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "LocalSite is private, but CreateSite also makes it federated", LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - Some(true), - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "CreateSite requires application, but neither it nor LocalSite has an application question", LemmyErrorType::ApplicationQuestionRequired, - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(RegistrationMode::RequireApplication), - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + registration_mode: Some(RegistrationMode::RequireApplication), + ..Default::default() + }, ), ]; @@ -414,95 +359,72 @@ mod tests { let valid_payloads = [ ( "No changes between LocalSite and CreateSite", - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + ..Default::default() + }, ), ( "CreateSite allows clearing and changing values", - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - Some(String::new()), - Some(String::new()), - Some(ListingType::All), - Some(SortType::Active), - Some(String::new()), - Some(false), - Some(true), - Some(String::new()), - Some(RegistrationMode::Open), - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + sidebar: Some(String::new()), + description: Some(String::new()), + application_question: Some(String::new()), + private_instance: Some(false), + default_post_listing_type: Some(ListingType::All), + default_post_sort_type: Some(PostSortType::Active), + slur_filter_regex: Some(String::new()), + federation_enabled: Some(true), + registration_mode: Some(RegistrationMode::Open), + ..Default::default() + }, ), ( "CreateSite clears existing slur filter regex", - &generate_local_site( - false, - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("foo site_name"), - None::, - None::, - None::, - None::, - Some(String::new()), - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("foo site_name"), + slur_filter_regex: Some(String::new()), + ..Default::default() + }, ), ( "LocalSite has application question and CreateSite now requires applications,", - &generate_local_site( - false, - None::, - true, - false, - Some(String::from("question")), - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(RegistrationMode::RequireApplication), - ), + &LocalSite { + site_setup: false, + application_question: Some(String::from("question")), + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + registration_mode: Some(RegistrationMode::RequireApplication), + ..Default::default() + }, ), ]; @@ -518,84 +440,4 @@ mod tests { ); }) } - - fn generate_local_site( - site_setup: bool, - site_slur_filter_regex: Option, - site_is_private: bool, - site_is_federated: bool, - site_application_question: Option, - site_registration_mode: RegistrationMode, - ) -> LocalSite { - LocalSite { - site_setup, - application_question: site_application_question, - private_instance: site_is_private, - slur_filter_regex: site_slur_filter_regex, - federation_enabled: site_is_federated, - registration_mode: site_registration_mode, - ..Default::default() - } - } - - // Allow the test helper function to have too many arguments. - // It's either this or generate the entire struct each time for testing. - #[allow(clippy::too_many_arguments)] - fn generate_create_site( - site_name: String, - site_description: Option, - site_sidebar: Option, - site_listing_type: Option, - site_sort_type: Option, - site_slur_filter_regex: Option, - site_is_private: Option, - site_is_federated: Option, - site_application_question: Option, - site_registration_mode: Option, - ) -> CreateSite { - CreateSite { - name: site_name, - sidebar: site_sidebar, - description: site_description, - icon: None, - banner: None, - enable_downvotes: None, - enable_nsfw: None, - community_creation_admin_only: None, - require_email_verification: None, - application_question: site_application_question, - private_instance: site_is_private, - default_theme: None, - default_post_listing_type: site_listing_type, - default_sort_type: site_sort_type, - legal_information: None, - application_email_admins: None, - hide_modlog_mod_names: None, - discussion_languages: None, - slur_filter_regex: site_slur_filter_regex, - actor_name_max_length: None, - rate_limit_message: None, - rate_limit_message_per_second: None, - rate_limit_post: None, - rate_limit_post_per_second: None, - rate_limit_register: None, - rate_limit_register_per_second: None, - rate_limit_image: None, - rate_limit_image_per_second: None, - rate_limit_comment: None, - rate_limit_comment_per_second: None, - rate_limit_search: None, - rate_limit_search_per_second: None, - federation_enabled: site_is_federated, - federation_debug: None, - captcha_enabled: None, - captcha_difficulty: None, - allowed_instances: None, - blocked_instances: None, - taglines: None, - registration_mode: site_registration_mode, - content_warning: None, - default_post_listing_mode: None, - } - } } diff --git a/crates/api_crud/src/site/mod.rs b/crates/api_crud/src/site/mod.rs index 0bf7cc279..48b819c38 100644 --- a/crates/api_crud/src/site/mod.rs +++ b/crates/api_crud/src/site/mod.rs @@ -40,12 +40,17 @@ pub fn application_question_check( } } +fn not_zero(val: Option) -> Option { + match val { + Some(0) => None, + v => v, + } +} + #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { - use crate::site::{application_question_check, site_default_post_listing_type_check}; + use crate::site::{application_question_check, not_zero, site_default_post_listing_type_check}; use lemmy_db_schema::{ListingType, RegistrationMode}; #[test] @@ -93,4 +98,11 @@ mod tests { RegistrationMode::RequireApplication ); } + + #[test] + fn test_not_zero() { + assert_eq!(None, not_zero(None)); + assert_eq!(None, not_zero(Some(0))); + assert_eq!(Some(5), not_zero(Some(5))); + } } diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index c633bebde..47fd1f154 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -5,19 +5,16 @@ use lemmy_api_common::{ }; use lemmy_db_schema::source::{ actor_language::{LocalUserLanguage, SiteLanguage}, + community_block::CommunityBlock, + instance_block::InstanceBlock, language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, + oauth_provider::OAuthProvider, + person_block::PersonBlock, tagline::Tagline, }; -use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; -use lemmy_db_views_actor::structs::{ - CommunityBlockView, - CommunityFollowerView, - CommunityModeratorView, - InstanceBlockView, - PersonBlockView, - PersonView, -}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; +use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView}; use lemmy_utils::{ error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, CACHE_DURATION_API, @@ -41,16 +38,16 @@ pub async fn get_site( // This data is independent from the user account so we can cache it across requests let mut site_response = CACHE .try_get_with::<_, LemmyError>((), async { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let admins = PersonView::admins(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; - let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; - let custom_emojis = - CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; + let tagline = Tagline::get_random(&mut context.pool()).await.ok(); + let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?; + let oauth_providers = + OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone()); + Ok(GetSiteResponse { site_view, admins, @@ -58,16 +55,19 @@ pub async fn get_site( my_user: None, all_languages, discussion_languages, - taglines, - custom_emojis, blocked_urls, + tagline, + oauth_providers: Some(oauth_providers), + admin_oauth_providers: Some(admin_oauth_providers), + taglines: vec![], + custom_emojis: vec![], }) }) .await .map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?; // Build the local user with parallel queries and add it to site response - site_response.my_user = if let Some(local_user_view) = local_user_view { + site_response.my_user = if let Some(ref local_user_view) = local_user_view { let person_id = local_user_view.person.id; let local_user_id = local_user_view.local_user.id; let pool = &mut context.pool(); @@ -81,16 +81,16 @@ pub async fn get_site( discussion_languages, ) = lemmy_db_schema::try_join_with_pool!(pool => ( |pool| CommunityFollowerView::for_person(pool, person_id), - |pool| CommunityBlockView::for_person(pool, person_id), - |pool| InstanceBlockView::for_person(pool, person_id), - |pool| PersonBlockView::for_person(pool, person_id), + |pool| CommunityBlock::for_person(pool, person_id), + |pool| InstanceBlock::for_person(pool, person_id), + |pool| PersonBlock::for_person(pool, person_id), |pool| CommunityModeratorView::for_person(pool, person_id, Some(&local_user_view.local_user)), |pool| LocalUserLanguage::read(pool, local_user_id) )) .with_lemmy_type(LemmyErrorType::SystemErrLogin)?; Some(MyUserInfo { - local_user_view, + local_user_view: local_user_view.clone(), follows, moderates, community_blocks, @@ -102,5 +102,13 @@ pub async fn get_site( None }; + // filter oauth_providers for public access + if !local_user_view + .map(|l| l.local_user.admin) + .unwrap_or_default() + { + site_response.admin_oauth_providers = None; + } + Ok(Json(site_response)) } diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index f6377038d..085ed69d1 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -1,3 +1,4 @@ +use super::not_zero; use crate::site::{application_question_check, site_default_post_listing_type_check}; use activitypub_federation::config::Data; use actix_web::web::Json; @@ -24,7 +25,6 @@ use lemmy_db_schema::{ local_site_url_blocklist::LocalSiteUrlBlocklist, local_user::LocalUser, site::{Site, SiteUpdateForm}, - tagline::Tagline, }, traits::Crud, utils::{diesel_string_update, diesel_url_update, naive_now}, @@ -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, }, }, }; @@ -52,9 +52,7 @@ pub async fn update_site( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let local_site = site_view.local_site; let site = site_view.site; @@ -101,16 +99,15 @@ pub async fn update_site( .ok(); let local_site_form = LocalSiteUpdateForm { - enable_downvotes: data.enable_downvotes, registration_mode: data.registration_mode, - enable_nsfw: data.enable_nsfw, community_creation_admin_only: data.community_creation_admin_only, require_email_verification: data.require_email_verification, application_question: diesel_string_update(data.application_question.as_deref()), private_instance: data.private_instance, default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, - default_sort_type: data.default_sort_type, + default_post_sort_type: data.default_post_sort_type, + default_comment_sort_type: data.default_comment_sort_type, legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, @@ -122,6 +119,11 @@ pub async fn update_site( captcha_difficulty: data.captcha_difficulty.clone(), reports_email_admins: data.reports_email_admins, default_post_listing_mode: data.default_post_listing_mode, + oauth_registration: data.oauth_registration, + post_upvotes: data.post_upvotes, + post_downvotes: data.post_downvotes, + comment_upvotes: data.comment_upvotes, + comment_downvotes: data.comment_downvotes, ..Default::default() }; @@ -131,17 +133,17 @@ pub async fn update_site( let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm { message: data.rate_limit_message, - message_per_second: data.rate_limit_message_per_second, + message_per_second: not_zero(data.rate_limit_message_per_second), post: data.rate_limit_post, - post_per_second: data.rate_limit_post_per_second, + post_per_second: not_zero(data.rate_limit_post_per_second), register: data.rate_limit_register, - register_per_second: data.rate_limit_register_per_second, + register_per_second: not_zero(data.rate_limit_register_per_second), image: data.rate_limit_image, - image_per_second: data.rate_limit_image_per_second, + image_per_second: not_zero(data.rate_limit_image_per_second), comment: data.rate_limit_comment, - comment_per_second: data.rate_limit_comment_per_second, + comment_per_second: not_zero(data.rate_limit_comment_per_second), search: data.rate_limit_search, - search_per_second: data.rate_limit_search_per_second, + search_per_second: not_zero(data.rate_limit_search_per_second), ..Default::default() }; @@ -188,12 +190,7 @@ pub async fn update_site( .with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?; } - let new_taglines = data.taglines.clone(); - let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; - - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let rate_limit_config = local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); @@ -201,7 +198,7 @@ pub async fn update_site( Ok(Json(SiteResponse { site_view, - taglines, + taglines: vec![], })) } @@ -222,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)?; } @@ -250,13 +247,16 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::site::update::validate_update_payload; use lemmy_api_common::site::EditSite; - use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode, SortType}; + use lemmy_db_schema::{ + source::local_site::LocalSite, + ListingType, + PostSortType, + RegistrationMode, + }; use lemmy_utils::error::LemmyErrorType; #[test] @@ -265,140 +265,94 @@ mod tests { ( "EditSite name matches LocalSite slur filter", LemmyErrorType::Slurs, - &generate_local_site( - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("foo site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("foo site_name")), + ..Default::default() + }, ), ( "EditSite name matches new slur filter", LemmyErrorType::Slurs, - &generate_local_site( - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("zeta site_name")), - None::, - None::, - None::, - None::, - Some(String::from("(zeta|alpha)")), - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("zeta site_name")), + slur_filter_regex: Some(String::from("(zeta|alpha)")), + ..Default::default() + }, ), ( "EditSite listing type is Subscribed, which is invalid", LemmyErrorType::InvalidDefaultPostListingType, - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - Some(ListingType::Subscribed), - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + default_post_listing_type: Some(ListingType::Subscribed), + ..Default::default() + }, ), ( "EditSite is both private and federated", LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - None::, - None::, - None::, - Some(true), - Some(true), - None::, - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + private_instance: Some(true), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "LocalSite is private, but EditSite also makes it federated", LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - Some(true), - None::, - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "EditSite requires application, but neither it nor LocalSite has an application question", LemmyErrorType::ApplicationQuestionRequired, - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(RegistrationMode::RequireApplication), - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + registration_mode: Some(RegistrationMode::RequireApplication), + ..Default::default() + }, ), ]; @@ -434,91 +388,65 @@ mod tests { let valid_payloads = [ ( "No changes between LocalSite and EditSite", - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite::default(), ), ( "EditSite allows clearing and changing values", - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - Some(String::new()), - Some(String::new()), - Some(ListingType::All), - Some(SortType::Active), - Some(String::new()), - Some(false), - Some(true), - Some(String::new()), - Some(RegistrationMode::Open), - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + sidebar: Some(String::new()), + description: Some(String::new()), + application_question: Some(String::new()), + private_instance: Some(false), + default_post_listing_type: Some(ListingType::All), + default_post_sort_type: Some(PostSortType::Active), + slur_filter_regex: Some(String::new()), + registration_mode: Some(RegistrationMode::Open), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "EditSite name passes slur filter regex", - &generate_local_site( - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("foo site_name")), - None::, - None::, - None::, - None::, - Some(String::new()), - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + registration_mode: RegistrationMode::Open, + federation_enabled: false, + ..Default::default() + }, + &EditSite { + name: Some(String::from("foo site_name")), + slur_filter_regex: Some(String::new()), + ..Default::default() + }, ), ( "LocalSite has application question and EditSite now requires applications,", - &generate_local_site( - None::, - true, - false, - Some(String::from("question")), - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(RegistrationMode::RequireApplication), - ), + &LocalSite { + application_question: Some(String::from("question")), + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + registration_mode: Some(RegistrationMode::RequireApplication), + ..Default::default() + }, ), ]; @@ -534,84 +462,4 @@ mod tests { ); }) } - - fn generate_local_site( - site_slur_filter_regex: Option, - site_is_private: bool, - site_is_federated: bool, - site_application_question: Option, - site_registration_mode: RegistrationMode, - ) -> LocalSite { - LocalSite { - application_question: site_application_question, - private_instance: site_is_private, - slur_filter_regex: site_slur_filter_regex, - federation_enabled: site_is_federated, - registration_mode: site_registration_mode, - ..Default::default() - } - } - - // Allow the test helper function to have too many arguments. - // It's either this or generate the entire struct each time for testing. - #[allow(clippy::too_many_arguments)] - fn generate_edit_site( - site_name: Option, - site_description: Option, - site_sidebar: Option, - site_listing_type: Option, - site_sort_type: Option, - site_slur_filter_regex: Option, - site_is_private: Option, - site_is_federated: Option, - site_application_question: Option, - site_registration_mode: Option, - ) -> EditSite { - EditSite { - name: site_name, - sidebar: site_sidebar, - description: site_description, - icon: None, - banner: None, - enable_downvotes: None, - enable_nsfw: None, - community_creation_admin_only: None, - require_email_verification: None, - application_question: site_application_question, - private_instance: site_is_private, - default_theme: None, - default_post_listing_type: site_listing_type, - default_sort_type: site_sort_type, - legal_information: None, - application_email_admins: None, - hide_modlog_mod_names: None, - discussion_languages: None, - slur_filter_regex: site_slur_filter_regex, - actor_name_max_length: None, - rate_limit_message: None, - rate_limit_message_per_second: None, - rate_limit_post: None, - rate_limit_post_per_second: None, - rate_limit_register: None, - rate_limit_register_per_second: None, - rate_limit_image: None, - rate_limit_image_per_second: None, - rate_limit_comment: None, - rate_limit_comment_per_second: None, - rate_limit_search: None, - rate_limit_search_per_second: None, - federation_enabled: site_is_federated, - federation_debug: None, - captcha_enabled: None, - captcha_difficulty: None, - allowed_instances: None, - blocked_instances: None, - blocked_urls: None, - taglines: None, - registration_mode: site_registration_mode, - reports_email_admins: None, - content_warning: None, - default_post_listing_mode: None, - } - } } diff --git a/crates/api_crud/src/tagline/create.rs b/crates/api_crud/src/tagline/create.rs new file mode 100644 index 000000000..f67a26f68 --- /dev/null +++ b/crates/api_crud/src/tagline/create.rs @@ -0,0 +1,38 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + tagline::{CreateTagline, TaglineResponse}, + utils::{get_url_blocklist, is_admin, local_site_to_slur_regex, process_markdown}, +}; +use lemmy_db_schema::{ + source::{ + local_site::LocalSite, + tagline::{Tagline, TaglineInsertForm}, + }, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn create_tagline( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let local_site = LocalSite::read(&mut context.pool()).await?; + + let slur_regex = local_site_to_slur_regex(&local_site); + let url_blocklist = get_url_blocklist(&context).await?; + let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; + + let tagline_form = TaglineInsertForm { content }; + + let tagline = Tagline::create(&mut context.pool(), &tagline_form).await?; + + Ok(Json(TaglineResponse { tagline })) +} diff --git a/crates/api_crud/src/tagline/delete.rs b/crates/api_crud/src/tagline/delete.rs new file mode 100644 index 000000000..9add3cfe6 --- /dev/null +++ b/crates/api_crud/src/tagline/delete.rs @@ -0,0 +1,25 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + tagline::DeleteTagline, + utils::is_admin, + SuccessResponse, +}; +use lemmy_db_schema::{source::tagline::Tagline, traits::Crud}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn delete_tagline( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + Tagline::delete(&mut context.pool(), data.id).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api_crud/src/tagline/list.rs b/crates/api_crud/src/tagline/list.rs new file mode 100644 index 000000000..21929f547 --- /dev/null +++ b/crates/api_crud/src/tagline/list.rs @@ -0,0 +1,19 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + tagline::{ListTaglines, ListTaglinesResponse}, +}; +use lemmy_db_schema::source::tagline::Tagline; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn list_taglines( + data: Query, + local_user_view: Option, + context: Data, +) -> Result, LemmyError> { + let taglines = Tagline::list(&mut context.pool(), data.page, data.limit).await?; + + Ok(Json(ListTaglinesResponse { taglines })) +} diff --git a/crates/api_crud/src/tagline/mod.rs b/crates/api_crud/src/tagline/mod.rs new file mode 100644 index 000000000..ffd48daf6 --- /dev/null +++ b/crates/api_crud/src/tagline/mod.rs @@ -0,0 +1,4 @@ +pub mod create; +pub mod delete; +pub mod list; +pub mod update; diff --git a/crates/api_crud/src/tagline/update.rs b/crates/api_crud/src/tagline/update.rs new file mode 100644 index 000000000..043589d26 --- /dev/null +++ b/crates/api_crud/src/tagline/update.rs @@ -0,0 +1,42 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + tagline::{TaglineResponse, UpdateTagline}, + utils::{get_url_blocklist, is_admin, local_site_to_slur_regex, process_markdown}, +}; +use lemmy_db_schema::{ + source::{ + local_site::LocalSite, + tagline::{Tagline, TaglineUpdateForm}, + }, + traits::Crud, + utils::naive_now, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn update_tagline( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let local_site = LocalSite::read(&mut context.pool()).await?; + + let slur_regex = local_site_to_slur_regex(&local_site); + let url_blocklist = get_url_blocklist(&context).await?; + let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; + + let tagline_form = TaglineUpdateForm { + content, + updated: naive_now(), + }; + + let tagline = Tagline::update(&mut context.pool(), data.id, &tagline_form).await?; + + Ok(Json(TaglineResponse { tagline })) +} diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 64bef8760..ed560e3d6 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -3,11 +3,14 @@ use actix_web::{web::Json, HttpRequest}; use lemmy_api_common::{ claims::Claims, context::LemmyContext, + oauth_provider::AuthenticateWithOauth, person::{LoginResponse, Register}, utils::{ + check_email_verified, + check_registration_application, + check_user_valid, generate_inbox_url, generate_local_apub_endpoint, - generate_shared_inbox_url, honeypot_check, local_site_to_slur_regex, password_length_check, @@ -18,11 +21,15 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::PersonAggregates, + newtypes::{InstanceId, OAuthProviderId}, source::{ captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, language::Language, + local_site::LocalSite, local_user::{LocalUser, LocalUserInsertForm}, local_user_vote_display_mode::LocalUserVoteDisplayMode, + oauth_account::{OAuthAccount, OAuthAccountInsertForm}, + oauth_provider::OAuthProvider, person::{Person, PersonInsertForm}, registration_application::{RegistrationApplication, RegistrationApplicationInsertForm}, }, @@ -31,23 +38,33 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ - error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, + error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ slurs::{check_slurs, check_slurs_opt}, validation::is_valid_actor_name, }, }; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use std::collections::HashSet; -#[tracing::instrument(skip(context))] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +/// Response from OAuth token endpoint +struct TokenResponse { + pub access_token: String, + pub token_type: String, + pub expires_in: Option, + pub refresh_token: Option, + pub scope: Option, +} + pub async fn register( data: Json, req: HttpRequest, context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let local_site = site_view.local_site; let require_registration_application = local_site.registration_mode == RegistrationMode::RequireApplication; @@ -63,8 +80,9 @@ pub async fn register( Err(LemmyErrorType::EmailRequired)? } - if local_site.site_setup && require_registration_application && data.answer.is_none() { - Err(LemmyErrorType::RegistrationApplicationAnswerRequired)? + // make sure the registration answer is provided when the registration application is required + if local_site.site_setup { + validate_registration_answer(require_registration_application, &data.answer)?; } // Make sure passwords match @@ -73,86 +91,50 @@ pub async fn register( } if local_site.site_setup && local_site.captcha_enabled { - if let Some(captcha_uuid) = &data.captcha_uuid { - let uuid = uuid::Uuid::parse_str(captcha_uuid)?; - let check = CaptchaAnswer::check_captcha( - &mut context.pool(), - CheckCaptchaAnswer { - uuid, - answer: data.captcha_answer.clone().unwrap_or_default(), - }, - ) - .await?; - if !check { - Err(LemmyErrorType::CaptchaIncorrect)? - } - } else { - Err(LemmyErrorType::CaptchaIncorrect)? - } + let uuid = uuid::Uuid::parse_str(&data.captcha_uuid.clone().unwrap_or_default())?; + CaptchaAnswer::check_captcha( + &mut context.pool(), + CheckCaptchaAnswer { + uuid, + answer: data.captcha_answer.clone().unwrap_or_default(), + }, + ) + .await?; } let slur_regex = local_site_to_slur_regex(&local_site); check_slurs(&data.username, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?; - let actor_keypair = generate_actor_keypair()?; - is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?; - let actor_id = generate_local_apub_endpoint( - EndpointType::Person, - &data.username, - &context.settings().get_protocol_and_hostname(), - )?; + Person::check_username_taken(&mut context.pool(), &data.username).await?; if let Some(email) = &data.email { - if LocalUser::is_email_taken(&mut context.pool(), email).await? { - Err(LemmyErrorType::EmailAlreadyExists)? - } + LocalUser::check_is_email_taken(&mut context.pool(), email).await?; } // We have to create both a person, and local_user - - // 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())?), - private_key: Some(actor_keypair.private_key), - ..PersonInsertForm::new( - data.username.clone(), - actor_keypair.public_key, - site_view.site.instance_id, - ) - }; - - // insert the person - let inserted_person = Person::create(&mut context.pool(), &person_form) - .await - .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; + let inserted_person = create_person( + data.username.clone(), + &local_site, + site_view.site.instance_id, + &context, + ) + .await?; // Automatically set their application as accepted, if they created this with open registration. // Also fixes a bug which allows users to log in when registrations are changed to closed. let accepted_application = Some(!require_registration_application); - // Get the user's preferred language using the Accept-Language header - let language_tags: Vec = req - .headers() - .get("Accept-Language") - .map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default())) - .iter() - .flatten() - // Remove the optional region code - .map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string()) - .collect(); - // Show nsfw content if param is true, or if content_warning exists let show_nsfw = data .show_nsfw .unwrap_or(site_view.site.content_warning.is_some()); + let language_tags = get_language_tags(&req); + // Create the local user let local_user_form = LocalUserInsertForm { email: data.email.as_deref().map(str::to_lowercase), - password_encrypted: data.password.to_string(), show_nsfw: Some(show_nsfw), accepted_application, default_listing_type: Some(local_site.default_post_listing_type), @@ -160,21 +142,10 @@ pub async fn register( interface_language: language_tags.first().cloned(), // If its the initial site setup, they are an admin admin: Some(!local_site.site_setup), - ..LocalUserInsertForm::new(inserted_person.id, data.password.to_string()) + ..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string())) }; - let all_languages = Language::read_all(&mut context.pool()).await?; - // use hashset to avoid duplicates - let mut language_ids = HashSet::new(); - for l in language_tags { - if let Some(found) = all_languages.iter().find(|all| all.code == l) { - language_ids.insert(found.id); - } - } - let language_ids = language_ids.into_iter().collect(); - - let inserted_local_user = - LocalUser::create(&mut context.pool(), &local_user_form, language_ids).await?; + let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?; if local_site.site_setup && require_registration_application { // Create the registration application @@ -207,29 +178,13 @@ pub async fn register( let jwt = Claims::generate(inserted_local_user.id, req, &context).await?; login_response.jwt = Some(jwt); } else { - if local_site.require_email_verification { - let local_user_view = LocalUserView { - local_user: inserted_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_person, - counts: PersonAggregates::default(), - }; - // we check at the beginning of this method that email is set - let email = local_user_view - .local_user - .email - .clone() - .expect("email was provided"); - - send_verification_email( - &local_user_view, - &email, - &mut context.pool(), - context.settings(), - ) - .await?; - login_response.verify_email_sent = true; - } + login_response.verify_email_sent = send_verification_email_if_required( + &context, + &local_site, + &inserted_local_user, + &inserted_person, + ) + .await?; if require_registration_application { login_response.registration_created = true; @@ -238,3 +193,386 @@ pub async fn register( Ok(Json(login_response)) } + +#[tracing::instrument(skip(context))] +pub async fn authenticate_with_oauth( + data: Json, + req: HttpRequest, + context: Data, +) -> LemmyResult> { + let site_view = SiteView::read_local(&mut context.pool()).await?; + let local_site = site_view.local_site.clone(); + + // validate inputs + if data.oauth_provider_id == OAuthProviderId(0) || data.code.is_empty() || data.code.len() > 300 { + return Err(LemmyErrorType::OauthAuthorizationInvalid)?; + } + + // validate the redirect_uri + let redirect_uri = &data.redirect_uri; + if redirect_uri.host_str().unwrap_or("").is_empty() + || !redirect_uri.path().eq(&String::from("/oauth/callback")) + || !redirect_uri.query().unwrap_or("").is_empty() + { + Err(LemmyErrorType::OauthAuthorizationInvalid)? + } + + // Fetch the OAUTH provider and make sure it's enabled + let oauth_provider_id = data.oauth_provider_id; + let oauth_provider = OAuthProvider::read(&mut context.pool(), oauth_provider_id) + .await + .ok() + .ok_or(LemmyErrorType::OauthAuthorizationInvalid)?; + + if !oauth_provider.enabled { + return Err(LemmyErrorType::OauthAuthorizationInvalid)?; + } + + let token_response = + oauth_request_access_token(&context, &oauth_provider, &data.code, redirect_uri.as_str()) + .await?; + + let user_info = oidc_get_user_info( + &context, + &oauth_provider, + token_response.access_token.as_str(), + ) + .await?; + + let oauth_user_id = read_user_info(&user_info, oauth_provider.id_claim.as_str())?; + + let mut login_response = LoginResponse { + jwt: None, + registration_created: false, + verify_email_sent: false, + }; + + // Lookup user by oauth_user_id + let mut local_user_view = + LocalUserView::find_by_oauth_id(&mut context.pool(), oauth_provider.id, &oauth_user_id).await; + + let local_user: LocalUser; + if let Ok(user_view) = local_user_view { + // user found by oauth_user_id => Login user + local_user = user_view.clone().local_user; + + check_user_valid(&user_view.person)?; + check_email_verified(&user_view, &site_view)?; + check_registration_application(&user_view, &site_view.local_site, &mut context.pool()).await?; + } else { + // user has never previously registered using oauth + + // prevent registration if registration is closed + if local_site.registration_mode == RegistrationMode::Closed { + Err(LemmyErrorType::RegistrationClosed)? + } + + // prevent registration if registration is closed for OAUTH providers + if !local_site.oauth_registration { + return Err(LemmyErrorType::OauthRegistrationClosed)?; + } + + // Extract the OAUTH email claim from the returned user_info + let email = read_user_info(&user_info, "email")?; + + let require_registration_application = + local_site.registration_mode == RegistrationMode::RequireApplication; + + // Lookup user by OAUTH email and link accounts + local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await; + + let person; + if let Ok(user_view) = local_user_view { + // user found by email => link and login if linking is allowed + + // we only allow linking by email when email_verification is required otherwise emails cannot + // be trusted + if oauth_provider.account_linking_enabled && site_view.local_site.require_email_verification { + // WARNING: + // If an admin switches the require_email_verification config from false to true, + // users who signed up before the switch could have accounts with unverified emails falsely + // marked as verified. + + check_user_valid(&user_view.person)?; + check_email_verified(&user_view, &site_view)?; + check_registration_application(&user_view, &site_view.local_site, &mut context.pool()) + .await?; + + // Link with OAUTH => Login user + let oauth_account_form = + OAuthAccountInsertForm::new(user_view.local_user.id, oauth_provider.id, oauth_user_id); + + OAuthAccount::create(&mut context.pool(), &oauth_account_form) + .await + .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + + local_user = user_view.local_user.clone(); + } else { + return Err(LemmyErrorType::EmailAlreadyExists)?; + } + } else { + // No user was found by email => Register as new user + + // make sure the registration answer is provided when the registration application is required + validate_registration_answer(require_registration_application, &data.answer)?; + + // make sure the username is provided + let username = data + .username + .as_ref() + .ok_or(LemmyErrorType::RegistrationUsernameRequired)?; + + let slur_regex = local_site_to_slur_regex(&local_site); + check_slurs(username, &slur_regex)?; + check_slurs_opt(&data.answer, &slur_regex)?; + + Person::check_username_taken(&mut context.pool(), username).await?; + + // We have to create a person, a local_user, and an oauth_account + person = create_person( + username.clone(), + &local_site, + site_view.site.instance_id, + &context, + ) + .await?; + + // Show nsfw content if param is true, or if content_warning exists + let show_nsfw = data + .show_nsfw + .unwrap_or(site_view.site.content_warning.is_some()); + + let language_tags = get_language_tags(&req); + + // Create the local user + let local_user_form = LocalUserInsertForm { + email: Some(str::to_lowercase(&email)), + show_nsfw: Some(show_nsfw), + accepted_application: Some(!require_registration_application), + email_verified: Some(oauth_provider.auto_verify_email), + post_listing_mode: Some(local_site.default_post_listing_mode), + interface_language: language_tags.first().cloned(), + // If its the initial site setup, they are an admin + admin: Some(!local_site.site_setup), + ..LocalUserInsertForm::new(person.id, None) + }; + + local_user = create_local_user(&context, language_tags, &local_user_form).await?; + + // Create the oauth account + let oauth_account_form = + OAuthAccountInsertForm::new(local_user.id, oauth_provider.id, oauth_user_id); + + OAuthAccount::create(&mut context.pool(), &oauth_account_form) + .await + .map_err(|_| LemmyErrorType::IncorrectLogin)?; + + // prevent sign in until application is accepted + if local_site.site_setup + && require_registration_application + && !local_user.accepted_application + && !local_user.admin + { + // Create the registration application + RegistrationApplication::create( + &mut context.pool(), + &RegistrationApplicationInsertForm { + local_user_id: local_user.id, + answer: data.answer.clone().expect("must have an answer"), + }, + ) + .await?; + + login_response.registration_created = true; + } + + // Check email is verified when required + login_response.verify_email_sent = + send_verification_email_if_required(&context, &local_site, &local_user, &person).await?; + } + } + + if !login_response.registration_created && !login_response.verify_email_sent { + let jwt = Claims::generate(local_user.id, req, &context).await?; + login_response.jwt = Some(jwt); + } + + return Ok(Json(login_response)); +} + +async fn create_person( + username: String, + local_site: &LocalSite, + instance_id: InstanceId, + context: &Data, +) -> Result { + let actor_keypair = generate_actor_keypair()?; + is_valid_actor_name(&username, local_site.actor_name_max_length as usize)?; + let actor_id = generate_local_apub_endpoint( + EndpointType::Person, + &username, + &context.settings().get_protocol_and_hostname(), + )?; + + // Register the new person + let person_form = PersonInsertForm { + actor_id: Some(actor_id.clone()), + inbox_url: Some(generate_inbox_url()?), + private_key: Some(actor_keypair.private_key), + ..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id) + }; + + // insert the person + let inserted_person = Person::create(&mut context.pool(), &person_form) + .await + .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; + + Ok(inserted_person) +} + +fn get_language_tags(req: &HttpRequest) -> Vec { + req + .headers() + .get("Accept-Language") + .map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default())) + .iter() + .flatten() + // Remove the optional region code + .map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string()) + .collect::>() +} + +async fn create_local_user( + context: &Data, + language_tags: Vec, + local_user_form: &LocalUserInsertForm, +) -> Result { + let all_languages = Language::read_all(&mut context.pool()).await?; + // use hashset to avoid duplicates + let mut language_ids = HashSet::new(); + for l in language_tags { + if let Some(found) = all_languages.iter().find(|all| all.code == l) { + language_ids.insert(found.id); + } + } + let language_ids = language_ids.into_iter().collect(); + + let inserted_local_user = + LocalUser::create(&mut context.pool(), local_user_form, language_ids).await?; + + Ok(inserted_local_user) +} + +async fn send_verification_email_if_required( + context: &Data, + local_site: &LocalSite, + local_user: &LocalUser, + person: &Person, +) -> LemmyResult { + let mut sent = false; + if !local_user.admin && local_site.require_email_verification && !local_user.email_verified { + let local_user_view = LocalUserView { + local_user: local_user.clone(), + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: person.clone(), + counts: PersonAggregates::default(), + }; + + send_verification_email( + &local_user_view, + &local_user + .email + .clone() + .expect("invalid verification email"), + &mut context.pool(), + context.settings(), + ) + .await?; + + sent = true; + } + Ok(sent) +} + +fn validate_registration_answer( + require_registration_application: bool, + answer: &Option, +) -> LemmyResult<()> { + if require_registration_application && answer.is_none() { + Err(LemmyErrorType::RegistrationApplicationAnswerRequired)? + } + + Ok(()) +} + +async fn oauth_request_access_token( + context: &Data, + oauth_provider: &OAuthProvider, + code: &str, + redirect_uri: &str, +) -> LemmyResult { + // Request an Access Token from the OAUTH provider + let response = context + .client() + .post(oauth_provider.token_endpoint.as_str()) + .header("Accept", "application/json") + .form(&[ + ("grant_type", "authorization_code"), + ("code", code), + ("redirect_uri", redirect_uri), + ("client_id", &oauth_provider.client_id), + ("client_secret", &oauth_provider.client_secret), + ]) + .send() + .await; + + let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?; + if !response.status().is_success() { + Err(LemmyErrorType::OauthLoginFailed)?; + } + + // Extract the access token + let token_response = response + .json::() + .await + .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + + Ok(token_response) +} + +async fn oidc_get_user_info( + context: &Data, + oauth_provider: &OAuthProvider, + access_token: &str, +) -> LemmyResult { + // Request the user info from the OAUTH provider + let response = context + .client() + .get(oauth_provider.userinfo_endpoint.as_str()) + .header("Accept", "application/json") + .bearer_auth(access_token) + .send() + .await; + + let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?; + if !response.status().is_success() { + Err(LemmyErrorType::OauthLoginFailed)?; + } + + // Extract the OAUTH user_id claim from the returned user_info + let user_info = response + .json::() + .await + .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + + Ok(user_info) +} + +fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult { + if let Some(value) = user_info.get(key) { + let result = serde_json::from_value::(value.clone()) + .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + return Ok(result); + } + Err(LemmyErrorType::OauthLoginFailed)? +} diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 363230d83..39598265a 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -8,7 +8,11 @@ use lemmy_api_common::{ utils::purge_user_account, SuccessResponse, }; -use lemmy_db_schema::source::{login_token::LoginToken, person::Person}; +use lemmy_db_schema::source::{ + login_token::LoginToken, + oauth_account::OAuthAccount, + person::Person, +}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -19,11 +23,12 @@ pub async fn delete_account( local_user_view: LocalUserView, ) -> LemmyResult> { // Verify the password - let valid: bool = verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = local_user_view + .local_user + .password_encrypted + .as_ref() + .and_then(|password_encrypted| verify(&data.password, password_encrypted).ok()) + .unwrap_or(false); if !valid { Err(LemmyErrorType::IncorrectLogin)? } @@ -31,6 +36,7 @@ pub async fn delete_account( if data.delete_content { purge_user_account(local_user_view.person.id, &context).await?; } else { + OAuthAccount::delete_user_accounts(&mut context.pool(), local_user_view.local_user.id).await?; Person::delete_account(&mut context.pool(), local_user_view.person.id).await?; } @@ -39,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/activities/block/block_user.json b/crates/apub/assets/lemmy/activities/block/block_user.json index a07d3786c..f6d6170c3 100644 --- a/crates/apub/assets/lemmy/activities/block/block_user.json +++ b/crates/apub/assets/lemmy/activities/block/block_user.json @@ -8,6 +8,6 @@ "type": "Block", "removeData": true, "summary": "spam post", - "expires": "2021-11-01T12:23:50.151874Z", + "endTime": "2021-11-01T12:23:50.151874Z", "id": "http://enterprise.lemmy.ml/activities/block/5d42fffb-0903-4625-86d4-0b39bb344fc2" } diff --git a/crates/apub/assets/lemmy/activities/block/undo_block_user.json b/crates/apub/assets/lemmy/activities/block/undo_block_user.json index 5dadc0781..922b5e777 100644 --- a/crates/apub/assets/lemmy/activities/block/undo_block_user.json +++ b/crates/apub/assets/lemmy/activities/block/undo_block_user.json @@ -11,7 +11,7 @@ "type": "Block", "removeData": true, "summary": "spam post", - "expires": "2021-11-01T12:23:50.151874Z", + "endTime": "2021-11-01T12:23:50.151874Z", "id": "http://enterprise.lemmy.ml/activities/block/726f43ab-bd0e-4ab3-89c8-627e976f553c" }, "cc": ["http://enterprise.lemmy.ml/c/main"], diff --git a/crates/apub/assets/lemmy/objects/group.json b/crates/apub/assets/lemmy/objects/group.json index 1b848a866..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](https://memory-alpha.fandom.com/wiki/USS_Enterprise_(NCC-1701-D))!.", + "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/assets/pleroma/objects/note.json b/crates/apub/assets/pleroma/objects/note.json index ff4b20d25..af61ff46e 100644 --- a/crates/apub/assets/pleroma/objects/note.json +++ b/crates/apub/assets/pleroma/objects/note.json @@ -10,7 +10,7 @@ "attachment": [], "attributedTo": "https://queer.hacktivis.me/users/lanodan", "cc": ["https://www.w3.org/ns/activitystreams#Public"], - "content": "@popolon Have what?", + "content": "Have what?", "context": "https://queer.hacktivis.me/contexts/34cba3d2-2f35-4169-aeff-56af9bfeb753", "conversation": "https://queer.hacktivis.me/contexts/34cba3d2-2f35-4169-aeff-56af9bfeb753", "id": "https://queer.hacktivis.me/objects/8d4973f4-53de-49cd-8c27-df160e16a9c2", diff --git a/crates/apub/assets/pleroma/objects/person.json b/crates/apub/assets/pleroma/objects/person.json index bc9008bab..fff9a2cba 100644 --- a/crates/apub/assets/pleroma/objects/person.json +++ b/crates/apub/assets/pleroma/objects/person.json @@ -41,7 +41,7 @@ "owner": "https://queer.hacktivis.me/users/lanodan", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsWOgdjSMc010qvxC3njI\nXJlFWMJ5gJ8QXCW/PajYdsHPM6d+jxBNJ6zp9/tIRa2m7bWHTSkuHQ7QthOpt6vu\n+dAWpKRLS607SPLItn/qUcyXvgN+H8shfyhMxvkVs9jXdtlBsLUVE7UNpN0dxzqe\nI79QWbf7o4amgaIWGRYB+OYMnIxKt+GzIkivZdSVSYjfxNnBYkMCeUxm5EpPIxKS\nP5bBHAVRRambD5NUmyKILuC60/rYuc/C+vmgpY2HCWFS2q6o34dPr9enwL6t4b3m\nS1t/EJHk9rGaaDqSGkDEfyQI83/7SDebWKuETMKKFLZi1vMgQIFuOYCIhN6bIiZm\npQIDAQAB\n-----END PUBLIC KEY-----\n\n" }, - "summary": "---
Website: https://hacktivis.me/
Lang: Français(natif), English(fluent), LSF(🤏~👌), русский (еле-еле),
Politics: Anarchist as in DIY/DIWO, freedom of association, anti-authoritarian, anti-identitarianism

Pronouns: meh, pick any, have fun
Timezone: Let's say Mars, I have a non-24h cycle
```
🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:
Pleroma maintainer (mostly backend)
BadWolf developer
Gentoo contributor

Dayjob: yogoko.fr

That person which uses HJKL in games

Just because computer bad: X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

banner from: https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db
Federation-bots: #nobot", + "summary": "---Lang: Français(natif), English(fluent), LSF(🤏~👌), русский (еле-еле),
Politics: Anarchist as in DIY/DIWO, freedom of association, anti-authoritarian, anti-identitarianism

Pronouns: meh, pick any, have fun
Timezone: Let's say Mars, I have a non-24h cycle
```
🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:
Pleroma maintainer (mostly backend)
BadWolf developer
Gentoo contributor

Dayjob: yogoko.fr

That person which uses HJKL in games

Just because computer bad: X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

banner from: https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db
Federation-bots: #nobot", "tag": [ { "icon": { diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 48408c4fb..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}, }; @@ -23,7 +25,7 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, - utils::{remove_user_data, remove_user_data_in_community}, + utils::{remove_or_restore_user_data, remove_or_restore_user_data_in_community}, }; use lemmy_db_schema::{ source::{ @@ -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(), @@ -74,7 +69,6 @@ impl BlockUser { &context.settings().get_protocol_and_hostname(), )?, audience, - expires, end_time: expires, }) } @@ -129,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(), @@ -147,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?; } @@ -157,10 +152,11 @@ impl ActivityHandler for BlockUser { #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; - let expires = self.expires.or(self.end_time).map(Into::into); + let expires = self.end_time.map(Into::into); let mod_person = self.actor.dereference(context).await?; let blocked_person = self.object.dereference(context).await?; let target = self.target.dereference(context).await?; + let reason = self.summary; match target { SiteOrCommunity::Site(_site) => { let blocked_person = Person::update( @@ -174,14 +170,15 @@ impl ActivityHandler for BlockUser { ) .await?; if self.remove_data.unwrap_or(false) { - remove_user_data(blocked_person.id, context).await?; + remove_or_restore_user_data(mod_person.id, blocked_person.id, true, &reason, context) + .await?; } // write mod log let form = ModBanForm { mod_person_id: mod_person.id, other_person_id: blocked_person.id, - reason: self.summary, + reason, banned: Some(true), expires, }; @@ -196,18 +193,21 @@ 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(); if self.remove_data.unwrap_or(false) { - remove_user_data_in_community(community.id, blocked_person.id, &mut context.pool()) - .await?; + remove_or_restore_user_data_in_community( + community.id, + mod_person.id, + blocked_person.id, + true, + &reason, + &mut context.pool(), + ) + .await?; } // write to mod log @@ -215,7 +215,7 @@ impl ActivityHandler for BlockUser { mod_person_id: mod_person.id, other_person_id: blocked_person.id, community_id: community.id, - reason: self.summary, + reason, banned: Some(true), expires, }; diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs index d42b62369..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}; @@ -22,11 +24,7 @@ use lemmy_db_schema::{ traits::Crud, utils::DbPool, }; -use lemmy_db_views::structs::SiteView; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{LemmyError, LemmyResult}; use serde::Deserialize; use url::Url; @@ -137,18 +135,12 @@ pub(crate) async fn send_ban_from_site( moderator: Person, banned_user: Person, reason: Option, - remove_data: Option, + remove_or_restore_data: Option, ban: bool, expires: Option, context: Data, ) -> LemmyResult<()> { - let site = SiteOrCommunity::Site( - SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)? - .site - .into(), - ); + let site = SiteOrCommunity::Site(Site::read_local(&mut context.pool()).await?.into()); let expires = check_expire_time(expires)?; // if the action affects a local user, federate to other instances @@ -158,7 +150,7 @@ pub(crate) async fn send_ban_from_site( &site, &banned_user.into(), &moderator.into(), - remove_data.unwrap_or(false), + remove_or_restore_data.unwrap_or(false), reason.clone(), expires, &context, @@ -169,6 +161,7 @@ pub(crate) async fn send_ban_from_site( &site, &banned_user.into(), &moderator.into(), + remove_or_restore_data.unwrap_or(false), reason.clone(), &context, ) @@ -188,7 +181,6 @@ pub(crate) async fn send_ban_from_community( ) -> LemmyResult<()> { let community: ApubCommunity = Community::read(&mut context.pool(), community_id) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let expires = check_expire_time(data.expires)?; @@ -197,7 +189,7 @@ pub(crate) async fn send_ban_from_community( &SiteOrCommunity::Community(community), &banned_person.into(), &mod_.into(), - data.remove_data.unwrap_or(false), + data.remove_or_restore_data.unwrap_or(false), data.reason.clone(), expires, &context, @@ -208,9 +200,20 @@ pub(crate) async fn send_ban_from_community( &SiteOrCommunity::Community(community), &banned_person.into(), &mod_.into(), + data.remove_or_restore_data.unwrap_or(false), data.reason.clone(), &context, ) .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 b92320b2d..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,11 +15,14 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::UndoType, public}, + kinds::activity::UndoType, protocol::verification::verify_domains_match, traits::{ActivityHandler, Actor}, }; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{ + context::LemmyContext, + utils::{remove_or_restore_user_data, remove_or_restore_user_data_in_community}, +}; use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, @@ -36,15 +41,12 @@ impl UndoBlockUser { target: &SiteOrCommunity, user: &ApubPerson, mod_: &ApubPerson, + restore_data: bool, reason: Option, 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, @@ -52,12 +54,13 @@ 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, id: id.clone(), audience, + restore_data: Some(restore_data), }; let mut inboxes = ActivitySendTargets::to_inbox(user.shared_inbox_or_inbox()); @@ -89,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(()) @@ -98,11 +100,12 @@ impl ActivityHandler for UndoBlockUser { #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; - let expires = self.object.expires.or(self.object.end_time).map(Into::into); + let expires = self.object.end_time.map(Into::into); let mod_person = self.actor.dereference(context).await?; 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, @@ -114,6 +117,11 @@ impl ActivityHandler for UndoBlockUser { ) .await?; + if self.restore_data.unwrap_or(false) { + remove_or_restore_user_data(mod_person.id, blocked_person.id, false, &None, context) + .await?; + } + // write mod log let form = ModBanForm { mod_person_id: mod_person.id, @@ -125,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, @@ -132,6 +141,18 @@ impl ActivityHandler for UndoBlockUser { }; CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form).await?; + if self.restore_data.unwrap_or(false) { + remove_or_restore_user_data_in_community( + community.id, + mod_person.id, + blocked_person.id, + false, + &None, + &mut context.pool(), + ) + .await?; + } + // write to mod log let form = ModBanFromCommunityForm { mod_person_id: mod_person.id, diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index aebf28217..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 @@ -213,14 +214,12 @@ async fn can_accept_activity_in_community( context: &Data, ) -> LemmyResult<()> { if let Some(community) = community { - if !community.local - && !CommunityFollower::has_local_followers(&mut context.pool(), community.id).await? - { - Err(LemmyErrorType::CommunityHasNoFollowers)? - } // Local only community can't federate if community.visibility != CommunityVisibility::Public { - return Err(LemmyErrorType::CouldntFindCommunity.into()); + return Err(LemmyErrorType::NotFound.into()); + } + if !community.local { + CommunityFollower::check_has_local_followers(&mut context.pool(), community.id).await? } } Ok(()) diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs index 4048a1469..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::{ @@ -36,10 +37,7 @@ use lemmy_db_schema::{ }, traits::{Crud, Joinable}, }; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl CollectionAdd { @@ -56,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()], @@ -82,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()], @@ -118,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(()) @@ -129,9 +127,7 @@ impl ActivityHandler for CollectionAdd { async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let (community, collection_type) = - Community::get_by_collection_url(&mut context.pool(), &self.target.into()) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + Community::get_by_collection_url(&mut context.pool(), &self.target.into()).await?; match collection_type { CollectionType::Moderators => { let new_mod = ObjectId::::from(self.object) @@ -188,11 +184,9 @@ pub(crate) async fn send_add_mod_to_community( let actor: ApubPerson = actor.into(); let community: ApubCommunity = Community::read(&mut context.pool(), community_id) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let updated_mod: ApubPerson = Person::read(&mut context.pool(), updated_mod_id) .await? - .ok_or(LemmyErrorType::CouldntFindPerson)? .into(); if added { CollectionAdd::send_add_mod(&community, &updated_mod, &actor, &context).await @@ -211,7 +205,6 @@ pub(crate) async fn send_feature_post( let post: ApubPost = post.into(); let community = Community::read(&mut context.pool(), post.community_id) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); if featured { CollectionAdd::send_add_featured_post(&community, &post, &actor, &context).await diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs index 634ca526c..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::{ @@ -31,10 +32,7 @@ use lemmy_db_schema::{ }, traits::{Crud, Joinable}, }; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl CollectionRemove { @@ -51,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(), @@ -77,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()], @@ -113,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(()) @@ -124,9 +122,7 @@ impl ActivityHandler for CollectionRemove { async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let (community, collection_type) = - Community::get_by_collection_url(&mut context.pool(), &self.target.into()) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + Community::get_by_collection_url(&mut context.pool(), &self.target.into()).await?; match collection_type { CollectionType::Moderators => { let remove_mod = ObjectId::::from(self.object) diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs index 322cd88c2..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; @@ -32,10 +33,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; #[async_trait::async_trait] @@ -52,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?; @@ -95,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?; @@ -132,7 +130,6 @@ pub(crate) async fn send_lock_post( ) -> LemmyResult<()> { let community: ApubCommunity = Community::read(&mut context.pool(), post.community_id) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let id = generate_activity_id( LockType::Lock, @@ -141,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, @@ -157,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/report.rs b/crates/apub/src/activities/community/report.rs index d1bec0b75..4966add34 100644 --- a/crates/apub/src/activities/community/report.rs +++ b/crates/apub/src/activities/community/report.rs @@ -29,10 +29,7 @@ use lemmy_db_schema::{ }, traits::{Crud, Reportable}, }; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl Report { @@ -70,9 +67,7 @@ impl Report { PostOrComment::Post(p) => p.creator_id, PostOrComment::Comment(c) => c.creator_id, }; - let object_creator = Person::read(&mut context.pool(), object_creator_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let object_creator = Person::read(&mut context.pool(), object_creator_id).await?; let object_creator_site: Option = Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id) .await? 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 89be8d49e..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}, }; @@ -42,7 +42,6 @@ use lemmy_db_schema::{ use lemmy_utils::{ error::{LemmyError, LemmyResult}, utils::mention::scrape_text_for_mentions, - LemmyErrorType, }; use url::Url; @@ -56,17 +55,11 @@ impl CreateOrUpdateNote { ) -> LemmyResult<()> { // TODO: might be helpful to add a comment method to retrieve community directly let post_id = comment.post_id; - let post = Post::read(&mut context.pool(), post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post = Post::read(&mut context.pool(), post_id).await?; let community_id = post.community_id; - let person: ApubPerson = Person::read(&mut context.pool(), person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)? - .into(); + let person: ApubPerson = Person::read(&mut context.pool(), person_id).await?.into(); let community: ApubCommunity = Community::read(&mut context.pool(), community_id) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let id = generate_activity_id( @@ -77,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, @@ -125,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())?; @@ -160,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 e8bfc36e6..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}, }; @@ -32,7 +32,7 @@ use lemmy_db_schema::{ }, traits::{Crud, Likeable}, }; -use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl CreateOrUpdatePage { @@ -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, @@ -66,13 +66,9 @@ impl CreateOrUpdatePage { context: Data, ) -> LemmyResult<()> { let community_id = post.community_id; - let person: ApubPerson = Person::read(&mut context.pool(), person_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)? - .into(); + let person: ApubPerson = Person::read(&mut context.pool(), person_id).await?.into(); let community: ApubCommunity = Community::read(&mut context.pool(), community_id) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let create_or_update = @@ -106,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 d203aacf2..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, @@ -176,8 +176,8 @@ pub(in crate::activities) async fn receive_remove_action( .await?; } // TODO these need to be implemented yet, for now, return errors - DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::CouldntFindPrivateMessage)?, - DeletableObjects::Person(_) => Err(LemmyErrorType::CouldntFindPerson)?, + DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::NotFound)?, + DeletableObjects::Person(_) => Err(LemmyErrorType::NotFound)?, } Ok(()) } diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs index c9d268e74..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::{ @@ -39,7 +40,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; use std::ops::Deref; use url::Url; @@ -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( @@ -87,16 +89,15 @@ pub(crate) async fn send_apub_delete_private_message( let recipient_id = pm.recipient_id; let recipient: ApubPerson = Person::read(&mut context.pool(), recipient_id) .await? - .ok_or(LemmyErrorType::CouldntFindPerson)? .into(); 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(()) @@ -110,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(); @@ -171,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) @@ -186,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 b50580852..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, @@ -156,8 +156,8 @@ impl UndoDelete { .await?; } // TODO these need to be implemented yet, for now, return errors - DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::CouldntFindPrivateMessage)?, - DeletableObjects::Person(_) => Err(LemmyErrorType::CouldntFindPerson)?, + DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::NotFound)?, + DeletableObjects::Person(_) => Err(LemmyErrorType::NotFound)?, } Ok(()) } diff --git a/crates/apub/src/activities/following/follow.rs b/crates/apub/src/activities/following/follow.rs index 97227835a..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::CouldntFindCommunity.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 d81e7cabf..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,18 +83,13 @@ 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(), ))? } let person_id = person.id; let community_id = community.id; - let is_banned = CommunityPersonBanView::get(&mut context.pool(), person_id, community_id).await?; - if is_banned { - Err(LemmyErrorType::PersonIsBannedFromCommunity)? - } else { - Ok(()) - } + CommunityPersonBanView::check(&mut context.pool(), person_id, community_id).await } /// Verify that mod action in community was performed by a moderator. @@ -106,14 +103,6 @@ pub(crate) async fn verify_mod_action( community: &Community, context: &Data, ) -> LemmyResult<()> { - let mod_ = mod_id.dereference(context).await?; - - let is_mod_or_admin = - CommunityView::is_mod_or_admin(&mut context.pool(), mod_.id, community.id).await?; - if is_mod_or_admin { - return Ok(()); - } - // mod action comes from the same instance as the community, so it was presumably done // by an instance admin. // TODO: federate instance admin status and check it here @@ -121,24 +110,47 @@ pub(crate) async fn verify_mod_action( return Ok(()); } - Err(LemmyErrorType::NotAModerator)? + let mod_ = mod_id.dereference(context).await?; + CommunityView::check_is_mod_or_admin(&mut context.pool(), mod_.id, community.id).await } 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(()) } @@ -146,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(()) } @@ -245,9 +257,7 @@ pub async fn match_outgoing_activities( CreateOrUpdatePage::send(post, creator_id, CreateOrUpdateType::Update, context).await } DeletePost(post, person, data) => { - let community = Community::read(&mut context.pool(), post.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community = Community::read(&mut context.pool(), post.community_id).await?; send_apub_delete_in_community( person, community, @@ -264,9 +274,7 @@ pub async fn match_outgoing_activities( reason, removed, } => { - let community = Community::read(&mut context.pool(), post.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community = Community::read(&mut context.pool(), post.community_id).await?; send_apub_delete_in_community( moderator, community, @@ -352,7 +360,7 @@ pub async fn match_outgoing_activities( moderator, banned_user, reason, - remove_data, + remove_or_restore_data, ban, expires, } => { @@ -360,7 +368,7 @@ pub async fn match_outgoing_activities( moderator, banned_user, reason, - remove_data, + remove_or_restore_data, ban, expires, context, @@ -383,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/activities/voting/vote.rs b/crates/apub/src/activities/voting/vote.rs index 324c8b300..1cdc81952 100644 --- a/crates/apub/src/activities/voting/vote.rs +++ b/crates/apub/src/activities/voting/vote.rs @@ -18,7 +18,7 @@ use activitypub_federation::{ traits::{ActivityHandler, Actor}, }; use lemmy_api_common::{context::LemmyContext, utils::check_bot_account}; -use lemmy_db_schema::source::local_site::LocalSite; +use lemmy_db_schema::{source::local_site::LocalSite, FederationMode}; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; @@ -68,12 +68,22 @@ impl ActivityHandler for Vote { check_bot_account(&actor.0)?; - let enable_downvotes = LocalSite::read(&mut context.pool()) + // Check for enabled federation votes + let local_site = LocalSite::read(&mut context.pool()) .await - .map(|l| l.enable_downvotes) - .unwrap_or(true); - if self.kind == VoteType::Dislike && !enable_downvotes { - // If this is a downvote but downvotes are ignored, only undo any existing vote + .unwrap_or_default(); + + let (downvote_setting, upvote_setting) = match object { + PostOrComment::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), + PostOrComment::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), + }; + + // Don't allow dislikes for either disabled, or local only votes + let downvote_fail = self.kind == VoteType::Dislike && downvote_setting != FederationMode::All; + let upvote_fail = self.kind == VoteType::Like && upvote_setting != FederationMode::All; + + if downvote_fail || upvote_fail { + // If this is a rejection, undo the vote match object { PostOrComment::Post(p) => undo_vote_post(actor, &p, context).await, PostOrComment::Comment(c) => undo_vote_comment(actor, &c, context).await, diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index 2d1fac449..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), @@ -117,13 +124,12 @@ impl InCommunity for AnnouncableActivities { CollectionRemove(a) => a.community(context).await, LockPost(a) => a.community(context).await, UndoLockPost(a) => a.community(context).await, - Page(_) => Err(LemmyErrorType::CouldntFindPost.into()), + Page(_) => Err(LemmyErrorType::NotFound.into()), } } } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 12d18110e..3e7a2f4eb 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -1,3 +1,4 @@ +use super::comment_sort_type_with_default; use crate::{ api::listing_type_with_default, fetcher::resolve_actor_identifier, @@ -11,10 +12,13 @@ use lemmy_api_common::{ utils::check_private_instance, }; use lemmy_db_schema::{ - source::{comment::Comment, community::Community, local_site::LocalSite}, + source::{comment::Comment, community::Community}, traits::Crud, }; -use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; +use lemmy_db_views::{ + comment_view::CommentQuery, + structs::{LocalUserView, SiteView}, +}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] @@ -23,8 +27,8 @@ pub async fn list_comments( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; - check_private_instance(&local_user_view, &local_site)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; + check_private_instance(&local_user_view, &site_view.local_site)?; let community_id = if let Some(name) = &data.community_name { Some( @@ -35,7 +39,12 @@ pub async fn list_comments( } else { data.community_id }; - let sort = data.sort; + let local_user_ref = local_user_view.as_ref().map(|u| &u.local_user); + let sort = Some(comment_sort_type_with_default( + data.sort, + local_user_ref, + &site_view.local_site, + )); let max_depth = data.max_depth; let saved_only = data.saved_only; @@ -52,18 +61,13 @@ pub async fn list_comments( let listing_type = Some(listing_type_with_default( data.type_, local_user_view.as_ref().map(|u| &u.local_user), - &local_site, + &site_view.local_site, community_id, )); // If a parent_id is given, fetch the comment to get the path let parent_path = if let Some(parent_id) = parent_id { - Some( - Comment::read(&mut context.pool(), parent_id) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)? - .path, - ) + Some(Comment::read(&mut context.pool(), parent_id).await?.path) } else { None }; @@ -87,7 +91,7 @@ pub async fn list_comments( limit, ..Default::default() } - .list(&mut context.pool()) + .list(&site_view.site, &mut context.pool()) .await .with_lemmy_type(LemmyErrorType::CouldntGetComments)?; diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 7ceafed8d..cdf24dbaa 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -1,5 +1,5 @@ use crate::{ - api::{listing_type_with_default, sort_type_with_default}, + api::{listing_type_with_default, post_sort_type_with_default}, fetcher::resolve_actor_identifier, objects::community::ApubCommunity, }; @@ -8,14 +8,14 @@ use actix_web::web::{Json, Query}; use lemmy_api_common::{ context::LemmyContext, post::{GetPosts, GetPostsResponse}, - utils::check_private_instance, + utils::{check_conflicting_like_filters, check_private_instance}, }; use lemmy_db_schema::source::community::Community; use lemmy_db_views::{ post_view::PostQuery, structs::{LocalUserView, PaginationCursor, SiteView}, }; -use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] pub async fn list_posts( @@ -23,9 +23,7 @@ pub async fn list_posts( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let local_site = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site.local_site)?; @@ -44,12 +42,11 @@ 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; - if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() { - return Err(LemmyError::from(LemmyErrorType::ContradictingFilters)); - } + check_conflicting_like_filters(liked_only, disliked_only)?; let local_user = local_user_view.as_ref().map(|u| &u.local_user); let listing_type = Some(listing_type_with_default( @@ -59,7 +56,7 @@ pub async fn list_posts( community_id, )); - let sort = Some(sort_type_with_default( + let sort = Some(post_sort_type_with_default( data.sort, local_user, &local_site.local_site, @@ -86,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/mod.rs b/crates/apub/src/api/mod.rs index dab2ace06..580be3228 100644 --- a/crates/apub/src/api/mod.rs +++ b/crates/apub/src/api/mod.rs @@ -1,8 +1,9 @@ use lemmy_db_schema::{ newtypes::CommunityId, source::{local_site::LocalSite, local_user::LocalUser}, + CommentSortType, ListingType, - SortType, + PostSortType, }; pub mod list_comments; @@ -33,16 +34,30 @@ fn listing_type_with_default( } } -/// Returns a default instance-level sort type, if none is given by the user. +/// Returns a default instance-level post sort type, if none is given by the user. /// Order is type, local user default, then site default. -fn sort_type_with_default( - type_: Option, +fn post_sort_type_with_default( + type_: Option, local_user: Option<&LocalUser>, local_site: &LocalSite, -) -> SortType { +) -> PostSortType { type_.unwrap_or( local_user - .map(|u| u.default_sort_type) - .unwrap_or(local_site.default_sort_type), + .map(|u| u.default_post_sort_type) + .unwrap_or(local_site.default_post_sort_type), + ) +} + +/// Returns a default instance-level comment sort type, if none is given by the user. +/// Order is type, local user default, then site default. +fn comment_sort_type_with_default( + type_: Option, + local_user: Option<&LocalUser>, + local_site: &LocalSite, +) -> CommentSortType { + type_.unwrap_or( + local_user + .map(|u| u.default_comment_sort_type) + .unwrap_or(local_site.default_comment_sort_type), ) } diff --git a/crates/apub/src/api/read_community.rs b/crates/apub/src/api/read_community.rs index 62fd6ec0b..f94769158 100644 --- a/crates/apub/src/api/read_community.rs +++ b/crates/apub/src/api/read_community.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::source::{ }; use lemmy_db_views::structs::LocalUserView; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] pub async fn get_community( @@ -36,8 +36,7 @@ pub async fn get_community( None => { let name = data.name.clone().unwrap_or_else(|| "main".to_string()); resolve_actor_identifier::(&name, &context, &local_user_view, true) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)? + .await? .id } }; @@ -56,12 +55,9 @@ pub async fn get_community( local_user, is_mod_or_admin, ) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + .await?; - let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; + let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; let site = read_site_for_actor(community_view.community.actor_id.clone(), &context).await?; diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index d61f6d9c5..fac68cd63 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -13,7 +13,7 @@ use lemmy_db_views::{ structs::{LocalUserView, SiteView}, }; use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView}; -use lemmy_utils::error::{LemmyErrorExt2, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] pub async fn read_person( @@ -26,9 +26,7 @@ pub async fn read_person( Err(LemmyErrorType::NoIdGiven)? } - let local_site = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let local_site = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site.local_site)?; @@ -37,20 +35,17 @@ pub async fn read_person( None => { if let Some(username) = &data.username { resolve_actor_identifier::(username, &context, &local_user_view, true) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindPerson)? + .await? .id } else { - Err(LemmyErrorType::CouldntFindPerson)? + Err(LemmyErrorType::NotFound)? } } }; // You don't need to return settings for the user, since this comes back with GetSite // `my_user` - let person_view = PersonView::read(&mut context.pool(), person_details_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let person_view = PersonView::read(&mut context.pool(), person_details_id).await?; let sort = data.sort; let page = data.page; @@ -90,7 +85,7 @@ pub async fn read_person( creator_id, ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await?; let moderates = CommunityModeratorView::for_person( diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index 3f2591241..04d489592 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -1,10 +1,10 @@ use crate::fetcher::{ + post_or_comment::PostOrComment, search::{search_query_to_object_id, search_query_to_object_id_local, SearchableObjects}, user_or_community::UserOrCommunity, }; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; -use diesel::NotFound; use lemmy_api_common::{ context::LemmyContext, site::{ResolveObject, ResolveObjectResponse}, @@ -27,18 +27,18 @@ pub async fn resolve_object( // if there's no personId then the JWT was missing or invalid. let is_authenticated = local_user_view.is_some(); - let res = if is_authenticated { + let res = if is_authenticated || cfg!(debug_assertions) { // user is fully authenticated; allow remote lookups as well. search_query_to_object_id(data.q.clone(), &context).await } else { // user isn't authenticated only allow a local search. search_query_to_object_id_local(&data.q, &context).await } - .with_lemmy_type(LemmyErrorType::CouldntFindObject)?; + .with_lemmy_type(LemmyErrorType::NotFound)?; convert_response(res, local_user_view, &mut context.pool()) .await - .with_lemmy_type(LemmyErrorType::CouldntFindObject) + .with_lemmy_type(LemmyErrorType::NotFound) } async fn convert_response( @@ -46,51 +46,145 @@ async fn convert_response( local_user_view: Option, pool: &mut DbPool<'_>, ) -> LemmyResult> { - use SearchableObjects::*; - 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 { - Post(p) => { - removed_or_deleted = p.deleted || p.removed; - res.post = Some( - PostView::read(pool, p.id, local_user.as_ref(), false) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?, - ) - } - Comment(c) => { - removed_or_deleted = c.deleted || c.removed; - res.comment = Some( - CommentView::read(pool, c.id, local_user.as_ref()) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?, - ) - } - PersonOrCommunity(p) => match *p { - UserOrCommunity::User(u) => { - removed_or_deleted = u.deleted; - res.person = Some( - PersonView::read(pool, u.id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?, - ) + SearchableObjects::PostOrComment(pc) => match *pc { + PostOrComment::Post(p) => { + res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), is_admin).await?) } + PostOrComment::Comment(c) => { + res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?) + } + }, + SearchableObjects::PersonOrCommunity(pc) => match *pc { + 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? - .ok_or(LemmyErrorType::CouldntFindCommunity)?, - ) + 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/search.rs b/crates/apub/src/api/search.rs index a048b64a7..cdc9bc55e 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -4,7 +4,7 @@ use actix_web::web::{Json, Query}; use lemmy_api_common::{ context::LemmyContext, site::{Search, SearchResponse}, - utils::{check_private_instance, is_admin}, + utils::{check_conflicting_like_filters, check_private_instance, is_admin}, }; use lemmy_db_schema::{source::community::Community, utils::post_to_comment_sort_type, SearchType}; use lemmy_db_views::{ @@ -12,8 +12,12 @@ use lemmy_db_views::{ post_view::PostQuery, structs::{LocalUserView, SiteView}, }; -use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_db_views_actor::{ + community_view::CommunityQuery, + person_view::PersonQuery, + structs::CommunitySortType, +}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn search( @@ -21,9 +25,7 @@ pub async fn search( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let local_site = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site.local_site)?; @@ -39,167 +41,136 @@ pub async fn search( // TODO no clean / non-nsfw searching rn - let q = data.q.clone(); - let page = data.page; - let limit = data.limit; - let sort = data.sort; - let listing_type = data.listing_type; - let search_type = data.type_.unwrap_or(SearchType::All); - let community_id = if let Some(name) = &data.community_name { + let Query(Search { + q, + community_id, + community_name, + creator_id, + type_, + sort, + listing_type, + page, + limit, + title_only, + post_url_only, + saved_only, + liked_only, + disliked_only, + }) = data; + + let q = q.clone(); + let search_type = type_.unwrap_or(SearchType::All); + let community_id = if let Some(name) = &community_name { Some( resolve_actor_identifier::(name, &context, &local_user_view, false) .await?, ) .map(|c| c.id) } else { - data.community_id + community_id }; - let creator_id = data.creator_id; let local_user = local_user_view.as_ref().map(|l| &l.local_user); + check_conflicting_like_filters(liked_only, disliked_only)?; + + let posts_query = PostQuery { + sort, + listing_type, + community_id, + creator_id, + local_user, + search_term: Some(q.clone()), + page, + limit, + title_only, + url_only: post_url_only, + liked_only, + disliked_only, + saved_only, + ..Default::default() + }; + + let comment_query = CommentQuery { + sort: sort.map(post_to_comment_sort_type), + listing_type, + search_term: Some(q.clone()), + community_id, + creator_id, + local_user, + page, + limit, + liked_only, + disliked_only, + saved_only, + ..Default::default() + }; + + let community_query = CommunityQuery { + sort: sort.map(CommunitySortType::from), + listing_type, + search_term: Some(q.clone()), + title_only, + local_user, + is_mod_or_admin: is_admin, + page, + limit, + ..Default::default() + }; + + let person_query = PersonQuery { + sort, + search_term: Some(q.clone()), + listing_type, + page, + limit, + }; + match search_type { SearchType::Posts => { - posts = PostQuery { - sort: (sort), - listing_type: (listing_type), - community_id: (community_id), - creator_id: (creator_id), - local_user, - search_term: (Some(q)), - page: (page), - limit: (limit), - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await?; + posts = posts_query + .list(&local_site.site, &mut context.pool()) + .await?; } SearchType::Comments => { - comments = CommentQuery { - sort: (sort.map(post_to_comment_sort_type)), - listing_type: (listing_type), - search_term: (Some(q)), - community_id: (community_id), - creator_id: (creator_id), - local_user, - page: (page), - limit: (limit), - ..Default::default() - } - .list(&mut context.pool()) - .await?; + comments = comment_query + .list(&local_site.site, &mut context.pool()) + .await?; } SearchType::Communities => { - communities = CommunityQuery { - sort: (sort), - listing_type: (listing_type), - search_term: (Some(q)), - local_user, - is_mod_or_admin: (is_admin), - page: (page), - limit: (limit), - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await?; + communities = community_query + .list(&local_site.site, &mut context.pool()) + .await?; } SearchType::Users => { - users = PersonQuery { - sort, - search_term: (Some(q)), - listing_type: (listing_type), - page: (page), - limit: (limit), - } - .list(&mut context.pool()) - .await?; + users = person_query.list(&mut context.pool()).await?; } SearchType::All => { // If the community or creator is included, dont search communities or users let community_or_creator_included = - data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some(); + community_id.is_some() || community_name.is_some() || creator_id.is_some(); - let q = data.q.clone(); + posts = posts_query + .list(&local_site.site, &mut context.pool()) + .await?; - posts = PostQuery { - sort: (sort), - listing_type: (listing_type), - community_id: (community_id), - creator_id: (creator_id), - local_user, - search_term: (Some(q)), - page: (page), - limit: (limit), - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await?; - - let q = data.q.clone(); - - comments = CommentQuery { - sort: (sort.map(post_to_comment_sort_type)), - listing_type: (listing_type), - search_term: (Some(q)), - community_id: (community_id), - creator_id: (creator_id), - local_user, - page: (page), - limit: (limit), - ..Default::default() - } - .list(&mut context.pool()) - .await?; - - let q = data.q.clone(); + comments = comment_query + .list(&local_site.site, &mut context.pool()) + .await?; communities = if community_or_creator_included { vec![] } else { - CommunityQuery { - sort: (sort), - listing_type: (listing_type), - search_term: (Some(q)), - local_user, - is_mod_or_admin: (is_admin), - page: (page), - limit: (limit), - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await? + community_query + .list(&local_site.site, &mut context.pool()) + .await? }; - let q = data.q.clone(); - users = if community_or_creator_included { vec![] } else { - PersonQuery { - sort, - search_term: (Some(q)), - listing_type: (listing_type), - page: (page), - limit: (limit), - } - .list(&mut context.pool()) - .await? + person_query.list(&mut context.pool()).await? }; } - SearchType::Url => { - posts = PostQuery { - sort: (sort), - listing_type: (listing_type), - community_id: (community_id), - creator_id: (creator_id), - url_search: (Some(q)), - local_user, - page: (page), - limit: (limit), - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await?; - } }; // Return the jwt diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index a0879b3c9..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}, @@ -103,18 +103,22 @@ pub async fn import_settings( context: Data, ) -> LemmyResult> { let person_form = PersonUpdateForm { - display_name: Some(data.display_name.clone()), - bio: Some(data.bio.clone()), - matrix_user_id: Some(data.matrix_id.clone()), + display_name: data.display_name.clone().map(Some), + bio: data.bio.clone().map(Some), + matrix_user_id: data.bio.clone().map(Some), bot_account: data.bot_account, ..Default::default() }; - Person::update(&mut context.pool(), local_user_view.person.id, &person_form).await?; + // ignore error in case form is empty + Person::update(&mut context.pool(), local_user_view.person.id, &person_form) + .await + .ok(); let local_user_form = LocalUserUpdateForm { show_nsfw: data.settings.as_ref().map(|s| s.show_nsfw), theme: data.settings.clone().map(|s| s.theme.clone()), - default_sort_type: data.settings.as_ref().map(|s| s.default_sort_type), + default_post_sort_type: data.settings.as_ref().map(|s| s.default_post_sort_type), + default_comment_sort_type: data.settings.as_ref().map(|s| s.default_comment_sort_type), default_listing_type: data.settings.as_ref().map(|s| s.default_listing_type), interface_language: data.settings.clone().map(|s| s.interface_language), show_avatars: data.settings.as_ref().map(|s| s.show_avatars), @@ -122,12 +126,10 @@ pub async fn import_settings( .settings .as_ref() .map(|s| s.send_notifications_to_email), - show_scores: data.settings.as_ref().map(|s| s.show_scores), show_bot_accounts: data.settings.as_ref().map(|s| s.show_bot_accounts), show_read_posts: data.settings.as_ref().map(|s| s.show_read_posts), open_links_in_new_tab: data.settings.as_ref().map(|s| s.open_links_in_new_tab), blur_nsfw: data.settings.as_ref().map(|s| s.blur_nsfw), - auto_expand: data.settings.as_ref().map(|s| s.auto_expand), infinite_scroll_enabled: data.settings.as_ref().map(|s| s.infinite_scroll_enabled), post_listing_mode: data.settings.as_ref().map(|s| s.post_listing_mode), ..Default::default() @@ -184,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(()) @@ -308,86 +309,65 @@ where }); Ok(failed_items.into_iter().join(",")) } -#[cfg(test)] -#[allow(clippy::indexing_slicing)] -mod tests { - use crate::api::user_settings_backup::{export_settings, import_settings, UserSettingsBackup}; - use activitypub_federation::config::Data; +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +pub(crate) mod tests { + use crate::api::user_settings_backup::{export_settings, import_settings}; + 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; - 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? - .ok_or(LemmyErrorType::CouldntFindLocalUser)?, - ) - } - #[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::builder() - .name("testcom".to_string()) - .title("testcom".to_string()) - .instance_id(export_user.person.instance_id) - .build(); - let community = Community::create(&mut context.pool(), &community_form).await?; + let community_form = CommunityInsertForm::new( + export_user.person.instance_id, + "testcom".to_string(), + "testcom".to_string(), + "pubkey".to_string(), + ); + 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? - .ok_or(LemmyErrorType::CouldntFindLocalUser)?; + let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?; assert_eq!( export_user.person.display_name, @@ -395,51 +375,12 @@ 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?; - Ok(()) - } - - #[tokio::test] - #[serial] - async fn test_settings_partial_import() -> LemmyResult<()> { - let context = LemmyContext::init_test_context().await; - - let export_user = - create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; - - let community_form = CommunityInsertForm::builder() - .name("testcom".to_string()) - .title("testcom".to_string()) - .instance_id(export_user.person.instance_id) - .build(); - let community = Community::create(&mut context.pool(), &community_form).await?; - let follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: export_user.person.id, - pending: false, - }; - CommunityFollower::follow(&mut context.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 backup2 = UserSettingsBackup { - followed_communities: backup.followed_communities.clone(), - ..Default::default() - }; - import_settings( - actix_web::web::Json(backup2), - import_user.clone(), - context.reset_request_count(), - ) - .await?; + LocalUser::delete(pool, export_user.local_user.id).await?; + LocalUser::delete(pool, import_user.local_user.id).await?; Ok(()) } @@ -447,9 +388,9 @@ 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?; @@ -464,7 +405,7 @@ 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; @@ -474,8 +415,36 @@ 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(()) + } + + #[tokio::test] + #[serial] + async fn import_partial_backup() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); + + 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\"}}")?; + import_settings( + Json(backup), + import_user.clone(), + context.reset_request_count(), + ) + .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 + assert_eq!(import_user.person.bio, import_user_updated.person.bio); + // local_user can be deserialized without id/person_id fields + assert_eq!("my_theme", import_user_updated.local_user.theme); + Ok(()) } } diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs index 8e5419c7e..c7b925f97 100644 --- a/crates/apub/src/collections/community_moderators.rs +++ b/crates/apub/src/collections/community_moderators.rs @@ -98,7 +98,7 @@ impl Collection for ApubCommunityModerators { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use super::*; diff --git a/crates/apub/src/collections/community_outbox.rs b/crates/apub/src/collections/community_outbox.rs index 8aca82d38..01199bc2b 100644 --- a/crates/apub/src/collections/community_outbox.rs +++ b/crates/apub/src/collections/community_outbox.rs @@ -18,12 +18,9 @@ use activitypub_federation::{ }; use futures::future::join_all; use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url}; -use lemmy_db_schema::{utils::FETCH_LIMIT_MAX, SortType}; -use lemmy_db_views::{post_view::PostQuery, structs::SiteView}; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_db_schema::{source::site::Site, utils::FETCH_LIMIT_MAX, PostSortType}; +use lemmy_db_views::post_view::PostQuery; +use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; #[derive(Clone, Debug)] @@ -38,14 +35,11 @@ impl Collection for ApubCommunityOutbox { #[tracing::instrument(skip_all)] async fn read_local(owner: &Self::Owner, data: &Data) -> LemmyResult { - let site = SiteView::read_local(&mut data.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)? - .site; + let site = Site::read_local(&mut data.pool()).await?; let post_views = PostQuery { community_id: Some(owner.id), - sort: Some(SortType::New), + sort: Some(PostSortType::New), limit: Some(FETCH_LIMIT_MAX), ..Default::default() } diff --git a/crates/apub/src/fetcher/markdown_links.rs b/crates/apub/src/fetcher/markdown_links.rs new file mode 100644 index 000000000..d83aae515 --- /dev/null +++ b/crates/apub/src/fetcher/markdown_links.rs @@ -0,0 +1,192 @@ +use super::{search::SearchableObjects, user_or_community::UserOrCommunity}; +use crate::fetcher::post_or_comment::PostOrComment; +use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; +use lemmy_api_common::{ + context::LemmyContext, + utils::{generate_local_apub_endpoint, EndpointType}, +}; +use lemmy_db_schema::{newtypes::InstanceId, source::instance::Instance}; +use lemmy_utils::{ + error::LemmyResult, + utils::markdown::image_links::{markdown_find_links, markdown_handle_title}, +}; +use url::Url; + +pub async fn markdown_rewrite_remote_links_opt( + src: Option, + context: &Data, +) -> Option { + match src { + Some(t) => Some(markdown_rewrite_remote_links(t, context).await), + None => None, + } +} + +/// Goes through all remote markdown links and attempts to resolve them as Activitypub objects. +/// If successful, the link is rewritten to a local link, so it can be viewed without leaving the +/// local instance. +/// +/// As it relies on ObjectId::dereference, it can only be used for incoming federated objects, not +/// for the API. +pub async fn markdown_rewrite_remote_links( + mut src: String, + context: &Data, +) -> String { + let links_offsets = markdown_find_links(&src); + + // Go through the collected links in reverse order + for (start, end) in links_offsets.into_iter().rev() { + let (url, extra) = markdown_handle_title(&src, start, end); + + if let Some(local_url) = to_local_url(url, context).await { + let mut local_url = local_url.to_string(); + // restore title + if let Some(extra) = extra { + local_url = format!("{local_url} {extra}"); + } + src.replace_range(start..end, local_url.as_str()); + } + } + + src +} + +pub(crate) async fn to_local_url(url: &str, context: &Data) -> Option { + let local_domain = &context.settings().get_protocol_and_hostname(); + let object_id = ObjectId::::parse(url).ok()?; + if object_id.inner().domain() == Some(local_domain) { + return None; + } + let dereferenced = object_id.dereference(context).await.ok()?; + match dereferenced { + SearchableObjects::PostOrComment(pc) => match *pc { + PostOrComment::Post(post) => { + generate_local_apub_endpoint(EndpointType::Post, &post.id.to_string(), local_domain) + } + PostOrComment::Comment(comment) => { + generate_local_apub_endpoint(EndpointType::Comment, &comment.id.to_string(), local_domain) + } + } + .ok() + .map(Into::into), + SearchableObjects::PersonOrCommunity(pc) => match *pc { + UserOrCommunity::User(user) => { + format_actor_url(&user.name, "u", user.instance_id, context).await + } + UserOrCommunity::Community(community) => { + format_actor_url(&community.name, "c", community.instance_id, context).await + } + } + .ok(), + } +} + +async fn format_actor_url( + name: &str, + kind: &str, + instance_id: InstanceId, + context: &LemmyContext, +) -> LemmyResult { + let local_protocol_and_hostname = context.settings().get_protocol_and_hostname(); + let local_hostname = &context.settings().hostname; + let instance = Instance::read(&mut context.pool(), instance_id).await?; + let url = if &instance.domain != local_hostname { + format!( + "{local_protocol_and_hostname}/{kind}/{name}@{}", + instance.domain + ) + } else { + format!("{local_protocol_and_hostname}/{kind}/{name}") + }; + Ok(Url::parse(&url)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use lemmy_db_schema::{ + source::{ + community::{Community, CommunityInsertForm}, + post::{Post, PostInsertForm}, + }, + traits::Crud, + }; + use lemmy_db_views::structs::LocalUserView; + use pretty_assertions::assert_eq; + use serial_test::serial; + + #[serial] + #[tokio::test] + async fn test_markdown_rewrite_remote_links() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?; + let community = Community::create( + &mut context.pool(), + &CommunityInsertForm::new( + instance.id, + "my_community".to_string(), + "My Community".to_string(), + "pubkey".to_string(), + ), + ) + .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 { + ap_id: Some(Url::parse("https://example.com/post/123")?.into()), + ..PostInsertForm::new("My post".to_string(), user.person.id, community.id) + }; + let post = Post::create(&mut context.pool(), &post_form).await?; + let markdown_local_post_url = format!("[link](https://lemmy-alpha/post/{})", post.id); + + let tests: Vec<_> = vec![ + ( + "rewrite remote post link", + format!("[link]({})", post.ap_id), + markdown_local_post_url.as_ref(), + ), + ( + "rewrite community link", + format!("[link]({})", community.actor_id), + "[link](https://lemmy-alpha/c/my_community@example.com)", + ), + ( + "dont rewrite local post link", + "[link](https://lemmy-alpha/post/2)".to_string(), + "[link](https://lemmy-alpha/post/2)", + ), + ( + "dont rewrite local community link", + "[link](https://lemmy-alpha/c/test)".to_string(), + "[link](https://lemmy-alpha/c/test)", + ), + ( + "dont rewrite non-fediverse link", + "[link](https://example.com/)".to_string(), + "[link](https://example.com/)", + ), + ( + "dont rewrite invalid url", + "[link](example-com)".to_string(), + "[link](example-com)", + ), + ]; + + let context = LemmyContext::init_test_context().await; + for (msg, input, expected) in &tests { + let result = markdown_rewrite_remote_links(input.to_string(), &context).await; + + assert_eq!( + &result, expected, + "Testing {}, with original input '{}'", + msg, input + ); + } + + Instance::delete(&mut context.pool(), instance.id).await?; + + Ok(()) + } +} diff --git a/crates/apub/src/fetcher/mod.rs b/crates/apub/src/fetcher/mod.rs index 68fc07d30..29202004f 100644 --- a/crates/apub/src/fetcher/mod.rs +++ b/crates/apub/src/fetcher/mod.rs @@ -10,6 +10,7 @@ use lemmy_db_schema::traits::ApubActor; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyError, LemmyResult}; +pub(crate) mod markdown_links; pub mod post_or_comment; pub mod search; pub mod site_or_community_or_user; diff --git a/crates/apub/src/fetcher/post_or_comment.rs b/crates/apub/src/fetcher/post_or_comment.rs index e352e1257..be48e8ebd 100644 --- a/crates/apub/src/fetcher/post_or_comment.rs +++ b/crates/apub/src/fetcher/post_or_comment.rs @@ -12,10 +12,7 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{LemmyError, LemmyResult}; use serde::Deserialize; use url::Url; @@ -29,7 +26,7 @@ pub enum PostOrComment { #[serde(untagged)] pub enum PageOrNote { Page(Box), - Note(Note), + Note(Box), } #[async_trait::async_trait] @@ -64,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?)), }) } @@ -84,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?), }) } } @@ -97,15 +94,9 @@ impl InCommunity for PostOrComment { PostOrComment::Comment(c) => { Post::read(&mut context.pool(), c.post_id) .await? - .ok_or(LemmyErrorType::CouldntFindPost)? .community_id } }; - Ok( - Community::read(&mut context.pool(), cid) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? - .into(), - ) + Ok(Community::read(&mut context.pool(), cid).await?.into()) } } diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 76c284820..e8c029106 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -1,8 +1,5 @@ -use crate::{ - fetcher::user_or_community::{PersonOrGroup, UserOrCommunity}, - objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost}, - protocol::objects::{note::Note, page::Page}, -}; +use super::post_or_comment::{PageOrNote, PostOrComment}; +use crate::fetcher::user_or_community::{PersonOrGroup, UserOrCommunity}; use activitypub_federation::{ config::Data, fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor}, @@ -54,16 +51,14 @@ pub(crate) async fn search_query_to_object_id_local( /// The types of ActivityPub objects that can be fetched directly by searching for their ID. #[derive(Debug)] pub(crate) enum SearchableObjects { - Post(ApubPost), - Comment(ApubComment), + PostOrComment(Box), PersonOrCommunity(Box), } #[derive(Deserialize)] #[serde(untagged)] pub(crate) enum SearchableKinds { - Page(Box), - Note(Note), + PageOrNote(Box), PersonOrGroup(Box), } @@ -75,8 +70,7 @@ impl Object for SearchableObjects { fn last_refreshed_at(&self) -> Option> { match self { - SearchableObjects::Post(p) => p.last_refreshed_at(), - SearchableObjects::Comment(c) => c.last_refreshed_at(), + SearchableObjects::PostOrComment(p) => p.last_refreshed_at(), SearchableObjects::PersonOrCommunity(p) => p.last_refreshed_at(), } } @@ -95,13 +89,9 @@ impl Object for SearchableObjects { if let Some(uc) = uc { return Ok(Some(SearchableObjects::PersonOrCommunity(Box::new(uc)))); } - let p = ApubPost::read_from_id(object_id.clone(), context).await?; - if let Some(p) = p { - return Ok(Some(SearchableObjects::Post(p))); - } - let c = ApubComment::read_from_id(object_id, context).await?; - if let Some(c) = c { - return Ok(Some(SearchableObjects::Comment(c))); + let pc = PostOrComment::read_from_id(object_id.clone(), context).await?; + if let Some(pc) = pc { + return Ok(Some(SearchableObjects::PostOrComment(Box::new(pc)))); } Ok(None) } @@ -109,25 +99,16 @@ impl Object for SearchableObjects { #[tracing::instrument(skip_all)] async fn delete(self, data: &Data) -> LemmyResult<()> { match self { - SearchableObjects::Post(p) => p.delete(data).await, - SearchableObjects::Comment(c) => c.delete(data).await, - SearchableObjects::PersonOrCommunity(pc) => match *pc { - UserOrCommunity::User(p) => p.delete(data).await, - UserOrCommunity::Community(c) => c.delete(data).await, - }, + SearchableObjects::PostOrComment(pc) => pc.delete(data).await, + SearchableObjects::PersonOrCommunity(pc) => pc.delete(data).await, } } async fn into_json(self, data: &Data) -> LemmyResult { + use SearchableObjects::*; Ok(match self { - SearchableObjects::Post(p) => SearchableKinds::Page(Box::new(p.into_json(data).await?)), - SearchableObjects::Comment(c) => SearchableKinds::Note(c.into_json(data).await?), - SearchableObjects::PersonOrCommunity(pc) => { - SearchableKinds::PersonOrGroup(Box::new(match *pc { - UserOrCommunity::User(p) => PersonOrGroup::Person(p.into_json(data).await?), - UserOrCommunity::Community(c) => PersonOrGroup::Group(c.into_json(data).await?), - })) - } + PostOrComment(pc) => SearchableKinds::PageOrNote(Box::new(pc.into_json(data).await?)), + PersonOrCommunity(pc) => SearchableKinds::PersonOrGroup(Box::new(pc.into_json(data).await?)), }) } @@ -137,24 +118,20 @@ impl Object for SearchableObjects { expected_domain: &Url, data: &Data, ) -> LemmyResult<()> { + use SearchableKinds::*; match apub { - SearchableKinds::Page(a) => ApubPost::verify(a, expected_domain, data).await, - SearchableKinds::Note(a) => ApubComment::verify(a, expected_domain, data).await, - SearchableKinds::PersonOrGroup(pg) => match pg.as_ref() { - PersonOrGroup::Person(a) => ApubPerson::verify(a, expected_domain, data).await, - PersonOrGroup::Group(a) => ApubCommunity::verify(a, expected_domain, data).await, - }, + PageOrNote(pn) => PostOrComment::verify(pn, expected_domain, data).await, + PersonOrGroup(pg) => UserOrCommunity::verify(pg, expected_domain, data).await, } } #[tracing::instrument(skip_all)] async fn from_json(apub: Self::Kind, context: &Data) -> LemmyResult { - use SearchableKinds as SAT; + use SearchableKinds::*; use SearchableObjects as SO; Ok(match apub { - SAT::Page(p) => SO::Post(ApubPost::from_json(*p, context).await?), - SAT::Note(n) => SO::Comment(ApubComment::from_json(n, context).await?), - SAT::PersonOrGroup(pg) => { + PageOrNote(pg) => SO::PostOrComment(Box::new(PostOrComment::from_json(*pg, context).await?)), + PersonOrGroup(pg) => { SO::PersonOrCommunity(Box::new(UserOrCommunity::from_json(*pg, 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 17711817e..41160234f 100644 --- a/crates/apub/src/http/comment.rs +++ b/crates/apub/src/http/comment.rs @@ -1,21 +1,17 @@ +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, source::{comment::Comment, community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; use serde::Deserialize; #[derive(Deserialize)] @@ -28,20 +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? - .ok_or(LemmyErrorType::CouldntFindComment)? - .into(); - let post = Post::read(&mut context.pool(), comment.post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; - let community = Community::read(&mut context.pool(), post.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; - check_community_public(&community)?; + 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_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 0f6ee57cb..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)] @@ -35,31 +33,18 @@ pub(crate) async fn get_apub_community_http( let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, true) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? + .ok_or(LemmyErrorType::NotFound)? .into(); 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, @@ -67,8 +52,8 @@ pub(crate) async fn get_apub_community_followers( ) -> LemmyResult { let community = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; - check_community_public(&community)?; + .ok_or(LemmyErrorType::NotFound)?; + 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::CouldntFindCommunity)? + .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) } @@ -97,9 +83,9 @@ pub(crate) async fn get_apub_community_moderators( let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)? + .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,25 +94,24 @@ 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::CouldntFindCommunity)? + .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) } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] 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::{ @@ -151,14 +136,16 @@ pub(crate) mod tests { Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?; create_local_site(context, instance.id).await?; - let community_form = CommunityInsertForm::builder() - .name("testcom6".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(instance.id) - .deleted(Some(deleted)) - .visibility(Some(visibility)) - .build(); + let community_form = CommunityInsertForm { + deleted: Some(deleted), + visibility: Some(visibility), + ..CommunityInsertForm::new( + instance.id, + "testcom6".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ) + }; let community = Community::create(&mut context.pool(), &community_form).await?; Ok((instance, community)) } @@ -169,24 +156,19 @@ pub(crate) mod tests { instance_id: InstanceId, ) -> LemmyResult<()> { // Create a local site, since this is necessary for community fetching. - let site_form = SiteInsertForm::builder() - .name("test site".to_string()) - .instance_id(instance_id) - .build(); + let site_form = SiteInsertForm::new("test site".to_string(), instance_id); let site = Site::create(&mut context.pool(), &site_form).await?; - let local_site_form = LocalSiteInsertForm::builder().site_id(site.id).build(); + let local_site_form = LocalSiteInsertForm::new(site.id); let local_site = LocalSite::create(&mut context.pool(), &local_site_form).await?; - let local_site_rate_limit_form = LocalSiteRateLimitInsertForm::builder() - .local_site_id(local_site.id) - .build(); + let local_site_rate_limit_form = LocalSiteRateLimitInsertForm::new(local_site.id); LocalSiteRateLimit::create(&mut context.pool(), &local_site_rate_limit_form).await?; Ok(()) } async fn decode_response(res: HttpResponse) -> LemmyResult { - let body = to_bytes(res.into_body()).await.unwrap(); + let body = to_bytes(res.into_body()).await.unwrap_or_default(); let body = std::str::from_utf8(&body)?; Ok(serde_json::from_str(body)?) } @@ -196,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 { @@ -215,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?; @@ -224,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?; @@ -236,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 { @@ -246,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; @@ -255,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?; @@ -268,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; @@ -283,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 6303dd1b0..d79cd3d55 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -1,24 +1,24 @@ 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, }; use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; -use http::{header::LOCATION, StatusCode}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ newtypes::DbUrl, 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; @@ -46,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 @@ -76,14 +76,14 @@ fn create_apub_tombstone_response>(id: T) -> LemmyResult HttpResponse { let mut res = HttpResponse::PermanentRedirect(); - res.insert_header((LOCATION, url.as_str())); + res.insert_header((actix_web::http::header::LOCATION, url.as_str())); res.finish() } @@ -108,8 +108,8 @@ pub(crate) async fn get_activity( ))? .into(); let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id) - .await? - .ok_or(LemmyErrorType::CouldntFindActivity)?; + .await + .map_err(|_| FederationError::CouldntFindActivity)?; let sensitive = activity.sensitive; if sensitive { @@ -120,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 { - return Err(LemmyErrorType::CouldntFindCommunity.into()); +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 ba2372fe8..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)] @@ -32,7 +25,7 @@ pub(crate) async fn get_apub_person_http( // TODO: this needs to be able to read deleted persons, so that it can send tombstones let person: ApubPerson = Person::read_from_name(&mut context.pool(), &user_name, true) .await? - .ok_or(LemmyErrorType::CouldntFindPerson)? + .ok_or(LemmyErrorType::NotFound)? .into(); if !person.deleted { @@ -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, @@ -63,7 +44,7 @@ pub(crate) async fn get_apub_person_outbox( ) -> LemmyResult { let person = Person::read_from_name(&mut context.pool(), &info.user_name, false) .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + .ok_or(LemmyErrorType::NotFound)?; let outbox_id = generate_outbox_url(&person.actor_id)?.into(); let outbox = EmptyOutbox::new(outbox_id)?; create_apub_response(&outbox) diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs index 513cba7ea..6afb9fc3e 100644 --- a/crates/apub/src/http/post.rs +++ b/crates/apub/src/http/post.rs @@ -1,21 +1,17 @@ +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, source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; use serde::Deserialize; #[derive(Deserialize)] @@ -28,17 +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? - .ok_or(LemmyErrorType::CouldntFindPost)? - .into(); - let community = Community::read(&mut context.pool(), post.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; - check_community_public(&community)?; + let post: ApubPost = Post::read(&mut context.pool(), id).await?.into(); + let community = Community::read(&mut context.pool(), post.community_id).await?; + + 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/http/site.rs b/crates/apub/src/http/site.rs index 54d3c0e32..95175a006 100644 --- a/crates/apub/src/http/site.rs +++ b/crates/apub/src/http/site.rs @@ -6,16 +6,12 @@ use crate::{ use activitypub_federation::{config::Data, traits::Object}; use actix_web::HttpResponse; use lemmy_api_common::context::LemmyContext; -use lemmy_db_views::structs::SiteView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_db_schema::source::site::Site; +use lemmy_utils::error::LemmyResult; use url::Url; pub(crate) async fn get_apub_site_http(context: Data) -> LemmyResult { - let site: ApubSite = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)? - .site - .into(); + let site: ApubSite = Site::read_local(&mut context.pool()).await?.into(); let apub = site.into_json(&context).await?; create_apub_response(&apub) 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 de472bd8a..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, }; @@ -99,21 +102,12 @@ async fn get_comment_parent_creator( comment: &Comment, ) -> LemmyResult { let parent_creator_id = if let Some(parent_comment_id) = comment.parent_comment_id() { - let parent_comment = Comment::read(pool, parent_comment_id) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let parent_comment = Comment::read(pool, parent_comment_id).await?; parent_comment.creator_id } else { let parent_post_id = comment.post_id; - let parent_post = Post::read(pool, parent_post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let parent_post = Post::read(pool, parent_post_id).await?; parent_post.creator_id }; - Ok( - Person::read(pool, parent_creator_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)? - .into(), - ) + Ok(Person::read(pool, parent_creator_id).await?.into()) } diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 466094b7f..b7c6a5f51 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,8 +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, @@ -11,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, }; @@ -32,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; @@ -91,35 +92,27 @@ impl Object for ApubComment { #[tracing::instrument(skip_all)] async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; - let creator = Person::read(&mut context.pool(), creator_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let creator = Person::read(&mut context.pool(), creator_id).await?; let post_id = self.post_id; - let post = Post::read(&mut context.pool(), post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post = Post::read(&mut context.pool(), post_id).await?; let community_id = post.community_id; - let community = Community::read(&mut context.pool(), community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community = Community::read(&mut context.pool(), community_id).await?; let in_reply_to = if let Some(comment_id) = self.parent_comment_id() { - let parent_comment = Comment::read(&mut context.pool(), comment_id) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let parent_comment = Comment::read(&mut context.pool(), comment_id).await?; parent_comment.ap_id.into() } else { post.ap_id.into() }; - let language = LanguageTag::new_single(self.language_id, &mut context.pool()).await?; + let language = Some(LanguageTag::new_single(self.language_id, &mut context.pool()).await?); let maa = collect_non_local_mentions(&self, community.actor_id.clone().into(), context).await?; let note = Note { 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), @@ -131,11 +124,14 @@ impl Object for ApubComment { distinguished: Some(self.distinguished), language, audience: Some(community.actor_id.into()), + attachment: vec![], }; Ok(note) } + /// Recursively fetches all parent comments. This can lead to a stack overflow so we need to + /// Box::pin all large futures on the heap. #[tracing::instrument(skip_all)] async fn verify( note: &Note, @@ -144,20 +140,30 @@ 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 = note.community(context).await?; + let community = Box::pin(note.community(context)).await?; + verify_visibility(¬e.to, ¬e.cc, &community)?; - check_apub_id_valid_with_strictness(note.id.inner(), community.local, context).await?; + Box::pin(check_apub_id_valid_with_strictness( + note.id.inner(), + community.local, + context, + )) + .await?; verify_is_remote_object(¬e.id, context)?; - verify_person_in_community(¬e.attributed_to, &community, context).await?; + Box::pin(verify_person_in_community( + ¬e.attributed_to, + &community, + context, + )) + .await?; - let (post, _) = note.get_parents(context).await?; - let creator = note.attributed_to.dereference(context).await?; + let (post, _) = Box::pin(note.get_parents(context)).await?; + let creator = Box::pin(note.attributed_to.dereference(context)).await?; let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &creator, community.id) .await .is_ok(); if post.locked && !is_mod_or_admin { - Err(LemmyErrorType::PostIsLocked)? + Err(FederationError::PostIsLocked)? } else { Ok(()) } @@ -176,9 +182,13 @@ 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 language_id = - LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?; + let content = markdown_rewrite_remote_links(content, context).await; + let language_id = Some( + LanguageTag::to_language_id_single(note.language.unwrap_or_default(), &mut context.pool()) + .await?, + ); let form = CommentInsertForm { creator_id: creator.id, @@ -239,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(()) } @@ -292,7 +302,7 @@ pub(crate) mod tests { let comment = ApubComment::from_json(json, &context).await?; assert_eq!(comment.ap_id, pleroma_url.into()); - assert_eq!(comment.content.len(), 64); + assert_eq!(comment.content.len(), 10); assert!(!comment.local); assert_eq!(context.request_count(), 1); diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 93c2c83ae..efa2c5247 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -1,10 +1,11 @@ use crate::{ activities::GetActorType, check_apub_id_valid, + fetcher::markdown_links::markdown_rewrite_remote_links_opt, 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, }, @@ -12,6 +13,7 @@ use crate::{ use activitypub_federation::{ config::Data, kinds::actor::GroupType, + protocol::values::MediaTypeHtml, traits::{Actor, Object}, }; use chrono::{DateTime, Utc}; @@ -37,6 +39,7 @@ use lemmy_db_schema::{ }, traits::{ApubActor, Crud}, utils::naive_now, + CommunityVisibility, }; use lemmy_db_views_actor::structs::CommunityFollowerView; use lemmy_utils::{ @@ -106,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), @@ -115,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) } @@ -145,34 +149,46 @@ 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 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 { - name: group.preferred_username.clone(), - title: group.name.unwrap_or(group.preferred_username.clone()), - description, published: group.published, updated: group.updated, deleted: Some(false), nsfw: Some(group.sensitive.unwrap_or(false)), actor_id: Some(group.id.into()), local: Some(false), - public_key: group.public_key.public_key_pem, last_refreshed_at: Some(naive_now()), icon, banner, + 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, - instance_id, featured_url: group.featured.clone().map(Into::into), - ..Default::default() + visibility, + ..CommunityInsertForm::new( + instance_id, + group.preferred_username.clone(), + group.name.unwrap_or(group.preferred_username.clone()), + group.public_key.public_key_pem, + ) }; let languages = LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).await?; @@ -222,7 +238,7 @@ impl Actor for ApubCommunity { } fn shared_inbox(&self) -> Option { - self.shared_inbox_url.clone().map(Into::into) + None } } @@ -293,9 +309,15 @@ 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), - Some(132) + 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?; diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index c67a223e0..a123c85ba 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -2,6 +2,7 @@ use super::verify_is_remote_object; use crate::{ activities::GetActorType, check_apub_id_valid_with_strictness, + fetcher::markdown_links::markdown_rewrite_remote_links_opt, local_site_data_cached, objects::read_from_string_or_source_opt, protocol::{ @@ -41,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; @@ -88,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)] @@ -143,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(); @@ -151,6 +151,7 @@ impl Object for ApubSite { let url_blocklist = get_url_blocklist(context).await?; let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.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(apub.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(apub.image.map(|i| i.url), context).await?; @@ -218,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 61ff04622..737579662 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -2,13 +2,11 @@ use super::verify_is_remote_object; use crate::{ activities::GetActorType, check_apub_id_valid_with_strictness, + fetcher::markdown_links::markdown_rewrite_remote_links_opt, 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, }, @@ -117,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(), @@ -156,6 +152,7 @@ impl Object for ApubPerson { let url_blocklist = get_url_blocklist(context).await?; let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source); let bio = process_markdown_opt(&bio, slur_regex, &url_blocklist, context).await?; + let bio = markdown_rewrite_remote_links_opt(bio, context).await; let avatar = proxy_image_link_opt_apub(person.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(person.image.map(|i| i.url), context).await?; @@ -180,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, }; @@ -209,7 +211,7 @@ impl Actor for ApubPerson { } fn shared_inbox(&self) -> Option { - self.shared_inbox_url.clone().map(Into::into) + None } } @@ -277,15 +279,18 @@ pub(crate) mod tests { assert_eq!(person.name, "lanodan"); assert!(!person.local); assert_eq!(context.request_count(), 0); - assert_eq!(person.bio.as_ref().map(std::string::String::len), Some(873)); + assert_eq!(person.bio.as_ref().map(std::string::String::len), Some(812)); cleanup((person, site), &context).await?; 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 44e842413..b72fa1728 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,6 +1,7 @@ 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, objects::{read_from_string_or_source_opt, verify_is_remote_object}, protocol::{ @@ -15,7 +16,6 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::public, protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match}, traits::Object, }; @@ -39,7 +39,7 @@ use lemmy_db_schema::{ }; use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_utils::{ - error::{LemmyError, LemmyErrorType, LemmyResult}, + error::{LemmyError, LemmyResult}, spawn_try_task, utils::{ markdown::markdown_to_html, @@ -107,14 +107,10 @@ impl Object for ApubPost { #[tracing::instrument(skip_all)] async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; - let creator = Person::read(&mut context.pool(), creator_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let creator = Person::read(&mut context.pool(), creator_id).await?; let community_id = self.community_id; - let community = Community::read(&mut context.pool(), community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; - let language = LanguageTag::new_single(self.language_id, &mut context.pool()).await?; + let community = Community::read(&mut context.pool(), community_id).await?; + let language = Some(LanguageTag::new_single(self.language_id, &mut context.pool()).await?); let attachment = self .url @@ -138,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)), @@ -175,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(()) } @@ -184,15 +180,12 @@ impl Object for ApubPost { let creator = page.creator()?.dereference(context).await?; let community = page.community(context).await?; if community.posting_restricted_to_mods { - let is_mod = CommunityModeratorView::is_community_moderator( + CommunityModeratorView::check_is_community_moderator( &mut context.pool(), community.id, creator.id, ) .await?; - if !is_mod { - Err(LemmyErrorType::OnlyModsCanPostInCommunity)? - } } let mut name = page .name @@ -233,10 +226,13 @@ impl Object for ApubPost { let url_blocklist = get_url_blocklist(context).await?; - if let Some(url) = &url { - is_url_blocked(url, &url_blocklist)?; - is_valid_url(url)?; - } + let url = if let Some(url) = url { + is_url_blocked(&url, &url_blocklist)?; + is_valid_url(&url)?; + to_local_url(url.as_str(), context).await.or(Some(url)) + } else { + None + }; let alt_text = first_attachment.cloned().and_then(Attachment::alt_text); @@ -244,24 +240,25 @@ impl Object for ApubPost { let body = read_from_string_or_source_opt(&page.content, &page.media_type, &page.source); let body = process_markdown_opt(&body, slur_regex, &url_blocklist, context).await?; - let language_id = - LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?; + let body = markdown_rewrite_remote_links_opt(body, context).await; + let language_id = Some( + LanguageTag::to_language_id_single(page.language.unwrap_or_default(), &mut context.pool()) + .await?, + ); - let form = PostInsertForm::builder() - .name(name) - .url(url.map(Into::into)) - .body(body) - .alt_text(alt_text) - .creator_id(creator.id) - .community_id(community.id) - .published(page.published.map(Into::into)) - .updated(page.updated.map(Into::into)) - .deleted(Some(false)) - .nsfw(page.sensitive) - .ap_id(Some(page.id.clone().into())) - .local(Some(false)) - .language_id(language_id) - .build(); + let form = PostInsertForm { + url: url.map(Into::into), + body, + alt_text, + published: page.published.map(Into::into), + updated: page.updated.map(Into::into), + deleted: Some(false), + nsfw: page.sensitive, + ap_id: Some(page.id.clone().into()), + local: Some(false), + language_id, + ..PostInsertForm::new(name, creator.id, community.id) + }; let timestamp = page.updated.or(page.published).unwrap_or_else(naive_now); let post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?; @@ -270,9 +267,9 @@ impl Object for ApubPost { // Generates a post thumbnail in background task, because some sites can be very slow to // respond. - spawn_try_task(async move { - generate_post_link_metadata(post_, None, |_| None, local_site, context_).await - }); + spawn_try_task( + async move { generate_post_link_metadata(post_, None, |_| None, context_).await }, + ); Ok(post.into()) } @@ -310,7 +307,7 @@ mod tests { assert_eq!(post.body.as_ref().map(std::string::String::len), Some(45)); assert!(!post.locked); assert!(!post.featured_community); - assert_eq!(context.request_count(), 0); + assert_eq!(context.request_count(), 1); Post::delete(&mut context.pool(), post.id).await?; Person::delete(&mut context.pool(), person.id).await?; diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index fc9697391..f3a9f140c 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -1,6 +1,7 @@ use super::verify_is_remote_object; use crate::{ check_apub_id_valid_with_strictness, + fetcher::markdown_links::markdown_rewrite_remote_links, objects::read_from_string_or_source, protocol::{ objects::chat_message::{ChatMessage, ChatMessageType}, @@ -15,19 +16,26 @@ use activitypub_federation::{ use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, - utils::{check_person_block, 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::{ local_site::LocalSite, person::Person, + person_block::PersonBlock, private_message::{PrivateMessage, PrivateMessageInsertForm}, }, 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; @@ -73,20 +81,16 @@ impl Object for ApubPrivateMessage { async fn delete(self, _context: &Data) -> LemmyResult<()> { // do nothing, because pm can't be fetched over http - Err(LemmyErrorType::CouldntFindPrivateMessage.into()) + Err(LemmyErrorType::NotFound.into()) } #[tracing::instrument(skip_all)] async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; - let creator = Person::read(&mut context.pool(), creator_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let creator = Person::read(&mut context.pool(), creator_id).await?; let recipient_id = self.recipient_id; - let recipient = Person::read(&mut context.pool(), recipient_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let recipient = Person::read(&mut context.pool(), recipient_id).await?; let note = ChatMessage { r#type: ChatMessageType::ChatMessage, @@ -115,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,13 +134,21 @@ impl Object for ApubPrivateMessage { ) -> LemmyResult { let creator = note.attributed_to.dereference(context).await?; let recipient = note.to[0].dereference(context).await?; - check_person_block(creator.id, recipient.id, &mut context.pool()).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; let form = PrivateMessageInsertForm { creator_id: creator.id, @@ -187,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/block/block_user.rs b/crates/apub/src/protocol/activities/block/block_user.rs index c1a4c64c7..96135d645 100644 --- a/crates/apub/src/protocol/activities/block/block_user.rs +++ b/crates/apub/src/protocol/activities/block/block_user.rs @@ -38,8 +38,6 @@ pub struct BlockUser { pub(crate) remove_data: Option, /// block reason, written to mod log pub(crate) summary: Option, - /// TODO: deprecated - pub(crate) expires: Option>, pub(crate) end_time: Option>, } diff --git a/crates/apub/src/protocol/activities/block/undo_block_user.rs b/crates/apub/src/protocol/activities/block/undo_block_user.rs index 491ec7ed1..e038fa2dc 100644 --- a/crates/apub/src/protocol/activities/block/undo_block_user.rs +++ b/crates/apub/src/protocol/activities/block/undo_block_user.rs @@ -29,6 +29,10 @@ pub struct UndoBlockUser { pub(crate) kind: UndoType, pub(crate) id: Url, pub(crate) audience: Option>, + + /// Quick and dirty solution. + /// TODO: send a separate Delete activity instead + pub(crate) restore_data: Option, } #[async_trait::async_trait] diff --git a/crates/apub/src/protocol/activities/community/collection_add.rs b/crates/apub/src/protocol/activities/community/collection_add.rs index 0e2ab75a6..777ad8b62 100644 --- a/crates/apub/src/protocol/activities/community/collection_add.rs +++ b/crates/apub/src/protocol/activities/community/collection_add.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::source::community::Community; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; use serde::{Deserialize, Serialize}; use url::Url; @@ -35,9 +35,7 @@ pub struct CollectionAdd { impl InCommunity for CollectionAdd { async fn community(&self, context: &Data) -> LemmyResult { let (community, _) = - Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()).await?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/community/collection_remove.rs b/crates/apub/src/protocol/activities/community/collection_remove.rs index 51c4761ba..afc0c24a0 100644 --- a/crates/apub/src/protocol/activities/community/collection_remove.rs +++ b/crates/apub/src/protocol/activities/community/collection_remove.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::source::community::Community; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; use serde::{Deserialize, Serialize}; use url::Url; @@ -35,9 +35,7 @@ pub struct CollectionRemove { impl InCommunity for CollectionRemove { async fn community(&self, context: &Data) -> LemmyResult { let (community, _) = - Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()).await?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/community/lock_page.rs b/crates/apub/src/protocol/activities/community/lock_page.rs index a08b3c5a2..5c8ecfca9 100644 --- a/crates/apub/src/protocol/activities/community/lock_page.rs +++ b/crates/apub/src/protocol/activities/community/lock_page.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::Crud}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; use serde::{Deserialize, Serialize}; use strum::Display; use url::Url; @@ -55,9 +55,7 @@ pub struct UndoLockPage { impl InCommunity for LockPage { async fn community(&self, context: &Data) -> LemmyResult { let post = self.object.dereference(context).await?; - let community = Community::read(&mut context.pool(), post.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community = Community::read(&mut context.pool(), post.community_id).await?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/community/report.rs b/crates/apub/src/protocol/activities/community/report.rs index 7698cde50..dd0f72f43 100644 --- a/crates/apub/src/protocol/activities/community/report.rs +++ b/crates/apub/src/protocol/activities/community/report.rs @@ -38,7 +38,7 @@ impl Report { .summary .clone() .or(self.content.clone()) - .ok_or(LemmyErrorType::CouldntFindObject.into()) + .ok_or(LemmyErrorType::NotFound.into()) } } @@ -63,7 +63,7 @@ impl ReportObject { return deref; } } - Err(LemmyErrorType::CouldntFindObject.into()) + Err(LemmyErrorType::NotFound.into()) } } } diff --git a/crates/apub/src/protocol/activities/create_or_update/note.rs b/crates/apub/src/protocol/activities/create_or_update/note.rs index 43ffeb291..ff0728174 100644 --- a/crates/apub/src/protocol/activities/create_or_update/note.rs +++ b/crates/apub/src/protocol/activities/create_or_update/note.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::Crud}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; use serde::{Deserialize, Serialize}; use url::Url; @@ -36,9 +36,7 @@ pub struct CreateOrUpdateNote { impl InCommunity for CreateOrUpdateNote { async fn community(&self, context: &Data) -> LemmyResult { let post = self.object.get_parents(context).await?.0; - let community = Community::read(&mut context.pool(), post.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community = Community::read(&mut context.pool(), post.community_id).await?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/deletion/delete.rs b/crates/apub/src/protocol/activities/deletion/delete.rs index 3b9aad079..3a29da069 100644 --- a/crates/apub/src/protocol/activities/deletion/delete.rs +++ b/crates/apub/src/protocol/activities/deletion/delete.rs @@ -15,7 +15,7 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use url::Url; @@ -51,9 +51,7 @@ impl InCommunity for Delete { let community_id = match DeletableObjects::read_from_db(self.object.id(), context).await? { DeletableObjects::Community(c) => c.id, DeletableObjects::Comment(c) => { - let post = Post::read(&mut context.pool(), c.post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post = Post::read(&mut context.pool(), c.post_id).await?; post.community_id } DeletableObjects::Post(p) => p.community_id, @@ -62,9 +60,7 @@ impl InCommunity for Delete { return Err(anyhow!("Private message is not part of community").into()) } }; - let community = Community::read(&mut context.pool(), community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community = Community::read(&mut context.pool(), community_id).await?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } 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 a9eb74e0c..00fe26d2b 100644 --- a/crates/apub/src/protocol/objects/mod.rs +++ b/crates/apub/src/protocol/objects/mod.rs @@ -30,21 +30,30 @@ pub(crate) struct LanguageTag { pub(crate) name: String, } +impl Default for LanguageTag { + fn default() -> Self { + LanguageTag { + identifier: "und".to_string(), + name: "Undetermined".to_string(), + } + } +} + impl LanguageTag { pub(crate) async fn new_single( lang: LanguageId, pool: &mut DbPool<'_>, - ) -> LemmyResult> { + ) -> LemmyResult { let lang = Language::read_from_id(pool, lang).await?; // undetermined if lang.id == UNDETERMINED_ID { - Ok(None) + Ok(LanguageTag::default()) } else { - Ok(Some(LanguageTag { + Ok(LanguageTag { identifier: lang.code, name: lang.name, - })) + }) } } @@ -69,13 +78,10 @@ impl LanguageTag { } pub(crate) async fn to_language_id_single( - lang: Option, + lang: Self, pool: &mut DbPool<'_>, - ) -> LemmyResult> { - let identifier = lang.map(|l| l.identifier); - let language = Language::read_id_from_code(pool, identifier.as_deref()).await?; - - Ok(language) + ) -> LemmyResult { + Ok(Language::read_id_from_code(pool, &lang.identifier).await?) } pub(crate) async fn to_language_id_multiple( @@ -86,10 +92,10 @@ impl LanguageTag { for l in langs { let id = l.identifier; - language_ids.push(Language::read_id_from_code(pool, Some(&id)).await?); + language_ids.push(Language::read_id_from_code(pool, &id).await?); } - Ok(language_ids.into_iter().flatten().collect()) + Ok(language_ids.into_iter().collect()) } } @@ -139,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 b0ae00037..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,10 +24,12 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::{ + error::{LemmyErrorType, LemmyResult}, + MAX_COMMENT_DEPTH_LIMIT, +}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use std::ops::Deref; use url::Url; #[skip_serializing_none] @@ -51,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 { @@ -58,15 +66,23 @@ impl Note { &self, context: &Data, ) -> LemmyResult<(ApubPost, Option)> { - // Fetch parent comment chain in a box, otherwise it can cause a stack overflow. - let parent = Box::pin(self.in_reply_to.dereference(context).await?); - match parent.deref() { + // We use recursion here to fetch the entire comment chain up to the top-level parent. This is + // necessary because we need to know the post and parent comment in order to insert a new + // comment. However it can also lead to stack overflow when fetching many comments recursively. + // To avoid this we check the request count against max comment depth, which based on testing + // can be handled without risking stack overflow. This is not a perfect solution, because in + // some cases we have to fetch user profiles too, and reach the limit after only 25 comments + // or so. + // A cleaner solution would be converting the recursion into a loop, but that is tricky. + if context.request_count() > MAX_COMMENT_DEPTH_LIMIT as u32 { + Err(LemmyErrorType::MaxCommentDepthReached)?; + } + let parent = self.in_reply_to.dereference(context).await?; + match parent { PostOrComment::Post(p) => Ok((p.clone(), None)), PostOrComment::Comment(c) => { let post_id = c.post_id; - let post = Post::read(&mut context.pool(), post_id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post = Post::read(&mut context.pool(), post_id).await?; Ok((post.into(), Some(c.clone()))) } } @@ -77,9 +93,7 @@ impl Note { impl InCommunity for Note { async fn community(&self, context: &Data) -> LemmyResult { let (post, _) = self.get_parents(context).await?; - let community = Community::read(&mut context.pool(), post.community_id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community = Community::read(&mut context.pool(), post.community_id).await?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs index 9c37c88c3..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()), } } } @@ -226,7 +245,7 @@ impl InCommunity for Page { break c; } } else { - Err(LemmyErrorType::CouldntFindCommunity)?; + Err(LemmyErrorType::NotFound)?; } } } @@ -234,7 +253,7 @@ impl InCommunity for Page { p.iter() .find(|a| a.kind == PersonOrGroupType::Group) .map(|a| ObjectId::::from(a.id.clone().into_inner())) - .ok_or(LemmyErrorType::CouldntFindCommunity)? + .ok_or(LemmyErrorType::NotFound)? .dereference(context) .await? } diff --git a/crates/db_perf/src/main.rs b/crates/db_perf/src/main.rs index 8e03a0a1d..02796a906 100644 --- a/crates/db_perf/src/main.rs +++ b/crates/db_perf/src/main.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ }, traits::Crud, utils::{build_db_pool, get_conn, now}, - SortType, + PostSortType, }; use lemmy_db_views::{post_view::PostQuery, structs::PaginationCursor}; use lemmy_utils::error::{LemmyErrorExt2, LemmyResult}; @@ -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?; @@ -79,11 +79,12 @@ async fn try_main() -> LemmyResult<()> { println!("🌍 creating {} communities", args.communities); let mut community_ids = vec![]; for i in 0..args.communities.get() { - let form = CommunityInsertForm::builder() - .name(format!("c{i}")) - .title(i.to_string()) - .instance_id(instance.id) - .build(); + let form = CommunityInsertForm::new( + instance.id, + format!("c{i}"), + i.to_string(), + "pubkey".to_string(), + ); community_ids.push(Community::create(&mut conn.into(), &form).await?.id); } @@ -151,7 +152,7 @@ async fn try_main() -> LemmyResult<()> { // TODO: include local_user let post_views = PostQuery { community_id: community_ids.as_slice().first().cloned(), - sort: Some(SortType::New), + sort: Some(PostSortType::New), limit: Some(20), page_after, ..Default::default() diff --git a/crates/db_perf/src/series.rs b/crates/db_perf/src/series.rs index b504efc54..8efc078b1 100644 --- a/crates/db_perf/src/series.rs +++ b/crates/db_perf/src/series.rs @@ -75,7 +75,7 @@ impl> ValidGrouping<()> type IsAggregate = is_aggregate::No; } -#[allow(non_camel_case_types)] +#[expect(non_camel_case_types)] #[derive(QueryId, Clone, Copy, Debug)] pub struct current_value; diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index bf9e4182f..2939228b6 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] @@ -64,7 +66,6 @@ diesel-async = { workspace = true, features = [ ], optional = true } regex = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true } -typed-builder = { workspace = true } async-trait = { workspace = true } tracing = { workspace = true } deadpool = { version = "0.12.1", features = ["rt_tokio_1"], optional = true } @@ -77,13 +78,12 @@ 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 } diff = "0.1.13" - -[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 92b24beb5..b26d27736 100644 --- a/crates/db_schema/src/aggregates/comment_aggregates.rs +++ b/crates/db_schema/src/aggregates/comment_aggregates.rs @@ -1,6 +1,5 @@ use crate::{ aggregates::structs::CommentAggregates, - diesel::OptionalExtension, newtypes::CommentId, schema::comment_aggregates, utils::{functions::hot_rank, get_conn, DbPool}, @@ -9,13 +8,9 @@ use diesel::{result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; impl CommentAggregates { - pub async fn read(pool: &mut DbPool<'_>, comment_id: CommentId) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, comment_id: CommentId) -> Result { let conn = &mut get_conn(pool).await?; - comment_aggregates::table - .find(comment_id) - .first(conn) - .await - .optional() + comment_aggregates::table.find(comment_id).first(conn).await } pub async fn update_hot_rank( @@ -35,8 +30,6 @@ impl CommentAggregates { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -51,76 +44,65 @@ mod tests { traits::{Crud, Likeable}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> Result<(), Error> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_comment_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_comment_agg"); - let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); + let another_inserted_person = Person::create(pool, &another_person).await?; - let new_community = CommunityInsertForm::builder() - .name("TIL_comment_agg".into()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_comment_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); - - let child_comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; let comment_like = CommentLikeForm { comment_id: inserted_comment.id, - post_id: inserted_post.id, person_id: inserted_person.id, score: 1, }; - CommentLike::like(pool, &comment_like).await.unwrap(); + CommentLike::like(pool, &comment_like).await?; - let comment_aggs_before_delete = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap() - .unwrap(); + let comment_aggs_before_delete = CommentAggregates::read(pool, inserted_comment.id).await?; assert_eq!(1, comment_aggs_before_delete.score); assert_eq!(1, comment_aggs_before_delete.upvotes); @@ -129,56 +111,43 @@ 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, }; - CommentLike::like(pool, &comment_dislike).await.unwrap(); + CommentLike::like(pool, &comment_dislike).await?; - let comment_aggs_after_dislike = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap() - .unwrap(); + let comment_aggs_after_dislike = CommentAggregates::read(pool, inserted_comment.id).await?; assert_eq!(0, comment_aggs_after_dislike.score); assert_eq!(1, comment_aggs_after_dislike.upvotes); assert_eq!(1, comment_aggs_after_dislike.downvotes); // Remove the first comment like - CommentLike::remove(pool, inserted_person.id, inserted_comment.id) - .await - .unwrap(); - let after_like_remove = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap() - .unwrap(); + CommentLike::remove(pool, inserted_person.id, inserted_comment.id).await?; + let after_like_remove = CommentAggregates::read(pool, inserted_comment.id).await?; assert_eq!(-1, after_like_remove.score); assert_eq!(0, after_like_remove.upvotes); assert_eq!(1, after_like_remove.downvotes); // Remove the parent post - Post::delete(pool, inserted_post.id).await.unwrap(); + Post::delete(pool, inserted_post.id).await?; // Should be none found, since the post was deleted - let after_delete = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap(); - assert!(after_delete.is_none()); + let after_delete = CommentAggregates::read(pool, inserted_comment.id).await; + assert!(after_delete.is_err()); // This should delete all the associated rows, and fire triggers - Person::delete(pool, another_inserted_person.id) - .await - .unwrap(); - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + Person::delete(pool, another_inserted_person.id).await?; + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/aggregates/community_aggregates.rs b/crates/db_schema/src/aggregates/community_aggregates.rs index fe9de62bb..3ec56d73d 100644 --- a/crates/db_schema/src/aggregates/community_aggregates.rs +++ b/crates/db_schema/src/aggregates/community_aggregates.rs @@ -1,6 +1,5 @@ use crate::{ aggregates::structs::CommunityAggregates, - diesel::OptionalExtension, newtypes::CommunityId, schema::{community_aggregates, community_aggregates::subscribers}, utils::{get_conn, DbPool}, @@ -9,16 +8,12 @@ use diesel::{result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; impl CommunityAggregates { - pub async fn read( - pool: &mut DbPool<'_>, - for_community_id: CommunityId, - ) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, for_community_id: CommunityId) -> Result { let conn = &mut get_conn(pool).await?; community_aggregates::table .find(for_community_id) .first(conn) .await - .optional() } pub async fn update_federated_followers( @@ -36,15 +31,19 @@ impl CommunityAggregates { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ 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,106 +51,93 @@ mod tests { traits::{Crud, Followable}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> Result<(), Error> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); - let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); + let another_inserted_person = Person::create(pool, &another_person).await?; - let new_community = CommunityInsertForm::builder() - .name("TIL_community_agg".into()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_community_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; - let inserted_community = Community::create(pool, &new_community).await.unwrap(); - - let another_community = CommunityInsertForm::builder() - .name("TIL_community_agg_2".into()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let another_inserted_community = Community::create(pool, &another_community).await.unwrap(); + let another_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_community_agg_2".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let another_inserted_community = Community::create(pool, &another_community).await?; 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 - .unwrap(); + CommunityFollower::follow(pool, &first_person_follow).await?; 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 - .unwrap(); + CommunityFollower::follow(pool, &second_person_follow).await?; 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 - .unwrap(); + CommunityFollower::follow(pool, &another_community_follow).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); - - let child_comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - let community_aggregates_before_delete = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + let community_aggregates_before_delete = + CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(2, community_aggregates_before_delete.subscribers); assert_eq!(2, community_aggregates_before_delete.subscribers_local); @@ -159,76 +145,53 @@ mod tests { assert_eq!(2, community_aggregates_before_delete.comments); // Test the other community - let another_community_aggs = CommunityAggregates::read(pool, another_inserted_community.id) - .await - .unwrap() - .unwrap(); + let another_community_aggs = + CommunityAggregates::read(pool, another_inserted_community.id).await?; assert_eq!(1, another_community_aggs.subscribers); assert_eq!(1, another_community_aggs.subscribers_local); assert_eq!(0, another_community_aggs.posts); assert_eq!(0, another_community_aggs.comments); // Unfollow test - CommunityFollower::unfollow(pool, &second_person_follow) - .await - .unwrap(); - let after_unfollow = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + CommunityFollower::unfollow(pool, &second_person_follow).await?; + let after_unfollow = CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(1, after_unfollow.subscribers); assert_eq!(1, after_unfollow.subscribers_local); // Follow again just for the later tests - CommunityFollower::follow(pool, &second_person_follow) - .await - .unwrap(); - let after_follow_again = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + CommunityFollower::follow(pool, &second_person_follow).await?; + let after_follow_again = CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(2, after_follow_again.subscribers); assert_eq!(2, after_follow_again.subscribers_local); // Remove a parent post (the comment count should also be 0) - Post::delete(pool, inserted_post.id).await.unwrap(); - let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + Post::delete(pool, inserted_post.id).await?; + let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(0, after_parent_post_delete.comments); assert_eq!(0, after_parent_post_delete.posts); // Remove the 2nd person - Person::delete(pool, another_inserted_person.id) - .await - .unwrap(); - let after_person_delete = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + Person::delete(pool, another_inserted_person.id).await?; + let after_person_delete = CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(1, after_person_delete.subscribers); assert_eq!(1, after_person_delete.subscribers_local); // This should delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); - let another_community_num_deleted = Community::delete(pool, another_inserted_community.id) - .await - .unwrap(); + let another_community_num_deleted = + Community::delete(pool, another_inserted_community.id).await?; assert_eq!(1, another_community_num_deleted); // Should be none found, since the creator was deleted - let after_delete = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap(); - assert!(after_delete.is_none()); + let after_delete = CommunityAggregates::read(pool, inserted_community.id).await; + assert!(after_delete.is_err()); + + Ok(()) } } diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index a8767895c..62aa9b609 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -1,4 +1,3 @@ -pub(crate) use crate::diesel::OptionalExtension; use crate::{ aggregates::structs::PersonAggregates, newtypes::PersonId, @@ -9,19 +8,13 @@ use diesel::{result::Error, QueryDsl}; use diesel_async::RunQueryDsl; impl PersonAggregates { - pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { let conn = &mut get_conn(pool).await?; - person_aggregates::table - .find(person_id) - .first(conn) - .await - .optional() + person_aggregates::table.find(person_id).first(conn).await } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -36,93 +29,81 @@ mod tests { traits::{Crud, Likeable}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> Result<(), Error> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_user_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_user_agg"); - let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); + let another_inserted_person = Person::create(pool, &another_person).await?; - let new_community = CommunityInsertForm::builder() - .name("TIL_site_agg".into()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_site_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; let post_like = PostLikeForm { post_id: inserted_post.id, person_id: inserted_person.id, score: 1, }; + let _inserted_post_like = PostLike::like(pool, &post_like).await?; - let _inserted_post_like = PostLike::like(pool, &post_like).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; let mut comment_like = CommentLikeForm { comment_id: inserted_comment.id, person_id: inserted_person.id, - post_id: inserted_post.id, score: 1, }; - let _inserted_comment_like = CommentLike::like(pool, &comment_like).await.unwrap(); - - let child_comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); + let _inserted_comment_like = CommentLike::like(pool, &comment_like).await?; + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); let inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; let child_comment_like = CommentLikeForm { comment_id: inserted_child_comment.id, person_id: another_inserted_person.id, - post_id: inserted_post.id, score: 1, }; - let _inserted_child_comment_like = CommentLike::like(pool, &child_comment_like).await.unwrap(); + let _inserted_child_comment_like = CommentLike::like(pool, &child_comment_like).await?; - let person_aggregates_before_delete = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + let person_aggregates_before_delete = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(1, person_aggregates_before_delete.post_count); assert_eq!(1, person_aggregates_before_delete.post_score); @@ -130,13 +111,8 @@ mod tests { assert_eq!(2, person_aggregates_before_delete.comment_score); // Remove a post like - PostLike::remove(pool, inserted_person.id, inserted_post.id) - .await - .unwrap(); - let after_post_like_remove = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; + let after_post_like_remove = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(0, after_post_like_remove.post_score); Comment::update( @@ -147,8 +123,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; Comment::update( pool, inserted_child_comment.id, @@ -157,51 +132,34 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let after_parent_comment_removed = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + let after_parent_comment_removed = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(0, after_parent_comment_removed.comment_count); // TODO: fix person aggregate comment score calculation // assert_eq!(0, after_parent_comment_removed.comment_score); // Remove a parent comment (the scores should also be removed) - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Comment::delete(pool, inserted_child_comment.id) - .await - .unwrap(); - let after_parent_comment_delete = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + Comment::delete(pool, inserted_comment.id).await?; + Comment::delete(pool, inserted_child_comment.id).await?; + let after_parent_comment_delete = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(0, after_parent_comment_delete.comment_count); // TODO: fix person aggregate comment score calculation // assert_eq!(0, after_parent_comment_delete.comment_score); // Add in the two comments again, then delete the post. - let new_parent_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let new_parent_comment = Comment::create(pool, &comment_form, None).await?; let _new_child_comment = - Comment::create(pool, &child_comment_form, Some(&new_parent_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&new_parent_comment.path)).await?; comment_like.comment_id = new_parent_comment.id; - CommentLike::like(pool, &comment_like).await.unwrap(); - let after_comment_add = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + CommentLike::like(pool, &comment_like).await?; + let after_comment_add = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(2, after_comment_add.comment_count); // TODO: fix person aggregate comment score calculation // assert_eq!(1, after_comment_add.comment_score); - Post::delete(pool, inserted_post.id).await.unwrap(); - let after_post_delete = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + Post::delete(pool, inserted_post.id).await?; + let after_post_delete = PersonAggregates::read(pool, inserted_person.id).await?; // TODO: fix person aggregate comment score calculation // assert_eq!(0, after_post_delete.comment_score); assert_eq!(0, after_post_delete.comment_count); @@ -209,24 +167,20 @@ mod tests { assert_eq!(0, after_post_delete.post_count); // This should delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); - Person::delete(pool, another_inserted_person.id) - .await - .unwrap(); + Person::delete(pool, another_inserted_person.id).await?; // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); // Should be none found - let after_delete = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap(); - assert!(after_delete.is_none()); + let after_delete = PersonAggregates::read(pool, inserted_person.id).await; + assert!(after_delete.is_err()); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } 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 eba3a02a3..46747b076 100644 --- a/crates/db_schema/src/aggregates/post_aggregates.rs +++ b/crates/db_schema/src/aggregates/post_aggregates.rs @@ -1,6 +1,5 @@ use crate::{ aggregates::structs::PostAggregates, - diesel::OptionalExtension, newtypes::PostId, schema::{community_aggregates, post, post_aggregates}, utils::{ @@ -13,13 +12,9 @@ use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; impl PostAggregates { - pub async fn read(pool: &mut DbPool<'_>, post_id: PostId) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, post_id: PostId) -> Result { let conn = &mut get_conn(pool).await?; - post_aggregates::table - .find(post_id) - .first(conn) - .await - .optional() + post_aggregates::table.find(post_id).first(conn).await } pub async fn update_ranks(pool: &mut DbPool<'_>, post_id: PostId) -> Result { @@ -54,8 +49,6 @@ impl PostAggregates { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -70,62 +63,55 @@ mod tests { traits::{Crud, Likeable}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> Result<(), Error> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); - let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); + let another_inserted_person = Person::create(pool, &another_person).await?; - let new_community = CommunityInsertForm::builder() - .name("TIL_community_agg".into()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_community_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); - - let child_comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); let inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; let post_like = PostLikeForm { post_id: inserted_post.id, @@ -133,12 +119,9 @@ mod tests { score: 1, }; - PostLike::like(pool, &post_like).await.unwrap(); + PostLike::like(pool, &post_like).await?; - let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id) - .await - .unwrap() - .unwrap(); + let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(2, post_aggs_before_delete.comments); assert_eq!(1, post_aggs_before_delete.score); @@ -152,12 +135,9 @@ mod tests { score: -1, }; - PostLike::like(pool, &post_dislike).await.unwrap(); + PostLike::like(pool, &post_dislike).await?; - let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id) - .await - .unwrap() - .unwrap(); + let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(2, post_aggs_after_dislike.comments); assert_eq!(0, post_aggs_after_dislike.score); @@ -165,95 +145,76 @@ mod tests { assert_eq!(1, post_aggs_after_dislike.downvotes); // Remove the comments - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Comment::delete(pool, inserted_child_comment.id) - .await - .unwrap(); - let after_comment_delete = PostAggregates::read(pool, inserted_post.id) - .await - .unwrap() - .unwrap(); + Comment::delete(pool, inserted_comment.id).await?; + Comment::delete(pool, inserted_child_comment.id).await?; + let after_comment_delete = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, after_comment_delete.comments); assert_eq!(0, after_comment_delete.score); assert_eq!(1, after_comment_delete.upvotes); assert_eq!(1, after_comment_delete.downvotes); // Remove the first post like - PostLike::remove(pool, inserted_person.id, inserted_post.id) - .await - .unwrap(); - let after_like_remove = PostAggregates::read(pool, inserted_post.id) - .await - .unwrap() - .unwrap(); + PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; + let after_like_remove = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, after_like_remove.comments); assert_eq!(-1, after_like_remove.score); assert_eq!(0, after_like_remove.upvotes); assert_eq!(1, after_like_remove.downvotes); // This should delete all the associated rows, and fire triggers - Person::delete(pool, another_inserted_person.id) - .await - .unwrap(); - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + Person::delete(pool, another_inserted_person.id).await?; + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); // Should be none found, since the creator was deleted - let after_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap(); - assert!(after_delete.is_none()); + let after_delete = PostAggregates::read(pool, inserted_post.id).await; + assert!(after_delete.is_err()); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_soft_delete() { - let pool = &build_db_pool_for_tests().await; + async fn test_soft_delete() -> Result<(), Error> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; - let new_community = CommunityInsertForm::builder() - .name("TIL_community_agg".into()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_community_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); - - let post_aggregates_before = PostAggregates::read(pool, inserted_post.id) - .await - .unwrap() - .unwrap(); + let post_aggregates_before = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(1, post_aggregates_before.comments); Comment::update( @@ -264,13 +225,9 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id) - .await - .unwrap() - .unwrap(); + let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, post_aggregates_after_remove.comments); Comment::update( @@ -281,8 +238,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; Comment::update( pool, @@ -292,13 +248,9 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id) - .await - .unwrap() - .unwrap(); + let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, post_aggregates_after_delete.comments); Comment::update( @@ -309,21 +261,17 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let post_aggregates_after_delete_remove = PostAggregates::read(pool, inserted_post.id) - .await - .unwrap() - .unwrap(); + let post_aggregates_after_delete_remove = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, post_aggregates_after_delete_remove.comments); - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Post::delete(pool, inserted_post.id).await.unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Comment::delete(pool, inserted_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Person::delete(pool, inserted_person.id).await?; + Community::delete(pool, inserted_community.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index ee9a1be9c..2df566290 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -1,6 +1,5 @@ use crate::{ aggregates::structs::SiteAggregates, - diesel::OptionalExtension, schema::site_aggregates, utils::{get_conn, DbPool}, }; @@ -8,15 +7,13 @@ use diesel::result::Error; use diesel_async::RunQueryDsl; impl SiteAggregates { - pub async fn read(pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>) -> Result { let conn = &mut get_conn(pool).await?; - site_aggregates::table.first(conn).await.optional() + site_aggregates::table.first(conn).await } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -32,83 +29,76 @@ mod tests { traits::Crud, utils::{build_db_pool_for_tests, DbPool}, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; async fn prepare_site_with_community( pool: &mut DbPool<'_>, - ) -> (Instance, Person, Site, Community) { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + ) -> Result<(Instance, Person, Site, Community), Error> { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_site_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; - let site_form = SiteInsertForm::builder() - .name("test_site".into()) - .instance_id(inserted_instance.id) - .build(); + let site_form = SiteInsertForm::new("test_site".into(), inserted_instance.id); + let inserted_site = Site::create(pool, &site_form).await?; - let inserted_site = Site::create(pool, &site_form).await.unwrap(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_site_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); - let new_community = CommunityInsertForm::builder() - .name("TIL_site_agg".into()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let inserted_community = Community::create(pool, &new_community).await?; - let inserted_community = Community::create(pool, &new_community).await.unwrap(); - ( + Ok(( inserted_instance, inserted_person, inserted_site, inserted_community, - ) + )) } #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (inserted_instance, inserted_person, inserted_site, inserted_community) = - prepare_site_with_community(pool).await; + prepare_site_with_community(pool).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); // Insert two of those posts - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - let _inserted_post_again = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; + let _inserted_post_again = Post::create(pool, &new_post).await?; - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); // Insert two of those comments - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); - - let child_comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - let site_aggregates_before_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_before_delete = SiteAggregates::read(pool).await?; // TODO: this is unstable, sometimes it returns 0 users, sometimes 1 //assert_eq!(0, site_aggregates_before_delete.users); @@ -117,42 +107,42 @@ mod tests { assert_eq!(2, site_aggregates_before_delete.comments); // Try a post delete - Post::delete(pool, inserted_post.id).await.unwrap(); - let site_aggregates_after_post_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); + Post::delete(pool, inserted_post.id).await?; + let site_aggregates_after_post_delete = SiteAggregates::read(pool).await?; assert_eq!(1, site_aggregates_after_post_delete.posts); assert_eq!(0, site_aggregates_after_post_delete.comments); // This shouuld delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); // Site should still exist, it can without a site creator. let after_delete_creator = SiteAggregates::read(pool).await; assert!(after_delete_creator.is_ok()); - Site::delete(pool, inserted_site.id).await.unwrap(); - let after_delete_site = SiteAggregates::read(pool).await.unwrap(); - assert!(after_delete_site.is_none()); + Site::delete(pool, inserted_site.id).await?; + let after_delete_site = SiteAggregates::read(pool).await; + assert!(after_delete_site.is_err()); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_soft_delete() { - let pool = &build_db_pool_for_tests().await; + async fn test_soft_delete() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (inserted_instance, inserted_person, inserted_site, inserted_community) = - prepare_site_with_community(pool).await; + prepare_site_with_community(pool).await?; - let site_aggregates_before = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_before = SiteAggregates::read(pool).await?; assert_eq!(1, site_aggregates_before.communities); Community::update( @@ -163,10 +153,9 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let site_aggregates_after_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_after_delete = SiteAggregates::read(pool).await?; assert_eq!(0, site_aggregates_after_delete.communities); Community::update( @@ -177,8 +166,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; Community::update( pool, @@ -188,10 +176,9 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let site_aggregates_after_remove = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_after_remove = SiteAggregates::read(pool).await?; assert_eq!(0, site_aggregates_after_remove.communities); Community::update( @@ -202,17 +189,16 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await?; assert_eq!(0, site_aggregates_after_remove_delete.communities); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Site::delete(pool, inserted_site.id).await.unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Community::delete(pool, inserted_community.id).await?; + Site::delete(pool, inserted_site.id).await?; + Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } 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 9391d55bc..d2cc6dcef 100644 --- a/crates/db_schema/src/impls/activity.rs +++ b/crates/db_schema/src/impls/activity.rs @@ -22,22 +22,15 @@ impl SentActivity { .await } - pub async fn read_from_apub_id( - pool: &mut DbPool<'_>, - object_id: &DbUrl, - ) -> Result, Error> { + pub async fn read_from_apub_id(pool: &mut DbPool<'_>, object_id: &DbUrl) -> Result { use crate::schema::sent_activity::dsl::{ap_id, sent_activity}; let conn = &mut get_conn(pool).await?; - sent_activity - .filter(ap_id.eq(object_id)) - .first(conn) - .await - .optional() + sent_activity.filter(ap_id.eq(object_id)).first(conn).await } - pub async fn read(pool: &mut DbPool<'_>, object_id: ActivityId) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, object_id: ActivityId) -> Result { use crate::schema::sent_activity::dsl::sent_activity; let conn = &mut get_conn(pool).await?; - sent_activity.find(object_id).first(conn).await.optional() + sent_activity.find(object_id).first(conn).await } } @@ -65,12 +58,11 @@ impl ReceivedActivity { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; use crate::{source::activity::ActorType, utils::build_db_pool_for_tests}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serde_json::json; use serial_test::serial; @@ -78,26 +70,25 @@ mod tests { #[tokio::test] #[serial] - async fn receive_activity_duplicate() { - let pool = &build_db_pool_for_tests().await; + async fn receive_activity_duplicate() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let ap_id: DbUrl = Url::parse("http://example.com/activity/531") - .unwrap() - .into(); + let ap_id: DbUrl = Url::parse("http://example.com/activity/531")?.into(); // inserting activity should only work once - ReceivedActivity::create(pool, &ap_id).await.unwrap(); - ReceivedActivity::create(pool, &ap_id).await.unwrap_err(); + ReceivedActivity::create(pool, &ap_id).await?; + let second = ReceivedActivity::create(pool, &ap_id).await; + assert!(second.is_err()); + + Ok(()) } #[tokio::test] #[serial] - async fn sent_activity_write_read() { - let pool = &build_db_pool_for_tests().await; + async fn sent_activity_write_read() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let ap_id: DbUrl = Url::parse("http://example.com/activity/412") - .unwrap() - .into(); + let ap_id: DbUrl = Url::parse("http://example.com/activity/412")?.into(); let data = json!({ "key1": "0xF9BA143B95FF6D82", "key2": "42", @@ -108,23 +99,20 @@ mod tests { ap_id: ap_id.clone(), data: data.clone(), sensitive, - actor_apub_id: Url::parse("http://example.com/u/exampleuser") - .unwrap() - .into(), + actor_apub_id: Url::parse("http://example.com/u/exampleuser")?.into(), actor_type: ActorType::Person, send_all_instances: false, send_community_followers_of: None, send_inboxes: vec![], }; - SentActivity::create(pool, form).await.unwrap(); + SentActivity::create(pool, form).await?; - let res = SentActivity::read_from_apub_id(pool, &ap_id) - .await - .unwrap() - .unwrap(); + let res = SentActivity::read_from_apub_id(pool, &ap_id).await?; assert_eq!(res.ap_id, ap_id); assert_eq!(res.data, data); assert_eq!(res.sensitive, sensitive); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index 5a8658baf..b4ad0d347 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -199,26 +199,22 @@ impl CommunityLanguage { /// Returns true if the given language is one of configured languages for given community pub async fn is_allowed_community_language( pool: &mut DbPool<'_>, - for_language_id: Option, + for_language_id: LanguageId, for_community_id: CommunityId, ) -> LemmyResult<()> { use crate::schema::community_language::dsl::community_language; let conn = &mut get_conn(pool).await?; - if let Some(for_language_id) = for_language_id { - let is_allowed = select(exists( - community_language.find((for_community_id, for_language_id)), - )) - .get_result(conn) - .await?; + let is_allowed = select(exists( + community_language.find((for_community_id, for_language_id)), + )) + .get_result(conn) + .await?; - if is_allowed { - Ok(()) - } else { - Err(LemmyErrorType::LanguageNotAllowed)? - } - } else { + if is_allowed { Ok(()) + } else { + Err(LemmyErrorType::LanguageNotAllowed)? } } @@ -327,7 +323,7 @@ pub async fn default_post_language( pool: &mut DbPool<'_>, community_id: CommunityId, local_user_id: LocalUserId, -) -> Result, Error> { +) -> Result { use crate::schema::{community_language::dsl as cl, local_user_language::dsl as ul}; let conn = &mut get_conn(pool).await?; let mut intersection = ul::local_user_language @@ -339,12 +335,12 @@ pub async fn default_post_language( .await?; if intersection.len() == 1 { - Ok(intersection.pop()) + Ok(intersection.pop().unwrap_or(UNDETERMINED_ID)) } else if intersection.len() == 2 && intersection.contains(&UNDETERMINED_ID) { intersection.retain(|i| i != &UNDETERMINED_ID); - Ok(intersection.pop()) + Ok(intersection.pop().unwrap_or(UNDETERMINED_ID)) } else { - Ok(None) + Ok(UNDETERMINED_ID) } } @@ -392,8 +388,7 @@ async fn convert_read_languages( } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use super::*; @@ -409,284 +404,238 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; - async fn test_langs1(pool: &mut DbPool<'_>) -> Vec { - vec![ - Language::read_id_from_code(pool, Some("en")) - .await - .unwrap() - .unwrap(), - Language::read_id_from_code(pool, Some("fr")) - .await - .unwrap() - .unwrap(), - Language::read_id_from_code(pool, Some("ru")) - .await - .unwrap() - .unwrap(), - ] + async fn test_langs1(pool: &mut DbPool<'_>) -> Result, Error> { + Ok(vec![ + Language::read_id_from_code(pool, "en").await?, + Language::read_id_from_code(pool, "fr").await?, + Language::read_id_from_code(pool, "ru").await?, + ]) } - async fn test_langs2(pool: &mut DbPool<'_>) -> Vec { - vec![ - Language::read_id_from_code(pool, Some("fi")) - .await - .unwrap() - .unwrap(), - Language::read_id_from_code(pool, Some("se")) - .await - .unwrap() - .unwrap(), - ] + async fn test_langs2(pool: &mut DbPool<'_>) -> Result, Error> { + Ok(vec![ + Language::read_id_from_code(pool, "fi").await?, + Language::read_id_from_code(pool, "se").await?, + ]) } - async fn create_test_site(pool: &mut DbPool<'_>) -> (Site, Instance) { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + async fn create_test_site(pool: &mut DbPool<'_>) -> Result<(Site, Instance), Error> { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let site_form = SiteInsertForm::builder() - .name("test site".to_string()) - .instance_id(inserted_instance.id) - .build(); - let site = Site::create(pool, &site_form).await.unwrap(); + let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id); + let site = Site::create(pool, &site_form).await?; // Create a local site, since this is necessary for local languages - let local_site_form = LocalSiteInsertForm::builder().site_id(site.id).build(); - LocalSite::create(pool, &local_site_form).await.unwrap(); + let local_site_form = LocalSiteInsertForm::new(site.id); + LocalSite::create(pool, &local_site_form).await?; - (site, inserted_instance) + Ok((site, inserted_instance)) } #[tokio::test] #[serial] - async fn test_convert_update_languages() { - let pool = &build_db_pool_for_tests().await; + async fn test_convert_update_languages() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); // call with empty vec, returns all languages - let conn = &mut get_conn(pool).await.unwrap(); - let converted1 = convert_update_languages(conn, vec![]).await.unwrap(); + let conn = &mut get_conn(pool).await?; + let converted1 = convert_update_languages(conn, vec![]).await?; assert_eq!(184, converted1.len()); // call with nonempty vec, returns same vec - let test_langs = test_langs1(&mut conn.into()).await; - let converted2 = convert_update_languages(conn, test_langs.clone()) - .await - .unwrap(); + let test_langs = test_langs1(&mut conn.into()).await?; + let converted2 = convert_update_languages(conn, test_langs.clone()).await?; assert_eq!(test_langs, converted2); + + Ok(()) } #[tokio::test] #[serial] - async fn test_convert_read_languages() { + 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 - let conn = &mut get_conn(pool).await.unwrap(); - let all_langs = language.select(id).get_results(conn).await.unwrap(); - let converted1: Vec = convert_read_languages(conn, all_langs).await.unwrap(); + let conn = &mut get_conn(pool).await?; + let all_langs = language.select(id).get_results(conn).await?; + let converted1: Vec = convert_read_languages(conn, all_langs).await?; assert_eq!(0, converted1.len()); // call with nonempty vec, returns same vec - let test_langs = test_langs1(&mut conn.into()).await; - let converted2 = convert_read_languages(conn, test_langs.clone()) - .await - .unwrap(); + let test_langs = test_langs1(&mut conn.into()).await?; + let converted2 = convert_read_languages(conn, test_langs.clone()).await?; assert_eq!(test_langs, converted2); + + Ok(()) } #[tokio::test] #[serial] - async fn test_site_languages() { - let pool = &build_db_pool_for_tests().await; + async fn test_site_languages() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let (site, instance) = create_test_site(pool).await; - let site_languages1 = SiteLanguage::read_local_raw(pool).await.unwrap(); + let (site, instance) = create_test_site(pool).await?; + let site_languages1 = SiteLanguage::read_local_raw(pool).await?; // site is created with all languages assert_eq!(184, site_languages1.len()); - let test_langs = test_langs1(pool).await; - SiteLanguage::update(pool, test_langs.clone(), &site) - .await - .unwrap(); + let test_langs = test_langs1(pool).await?; + SiteLanguage::update(pool, test_langs.clone(), &site).await?; - let site_languages2 = SiteLanguage::read_local_raw(pool).await.unwrap(); + let site_languages2 = SiteLanguage::read_local_raw(pool).await?; // after update, site only has new languages assert_eq!(test_langs, site_languages2); - Site::delete(pool, site.id).await.unwrap(); - Instance::delete(pool, instance.id).await.unwrap(); - LocalSite::delete(pool).await.unwrap(); + Site::delete(pool, site.id).await?; + Instance::delete(pool, instance.id).await?; + LocalSite::delete(pool).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_user_languages() { - let pool = &build_db_pool_for_tests().await; + async fn test_user_languages() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let (site, instance) = create_test_site(pool).await; + let (site, instance) = create_test_site(pool).await?; let person_form = PersonInsertForm::test_form(instance.id, "my test person"); - let person = Person::create(pool, &person_form).await.unwrap(); + let person = Person::create(pool, &person_form).await?; let local_user_form = LocalUserInsertForm::test_form(person.id); - let local_user = LocalUser::create(pool, &local_user_form, vec![]) - .await - .unwrap(); - let local_user_langs1 = LocalUserLanguage::read(pool, local_user.id).await.unwrap(); + let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; + let local_user_langs1 = LocalUserLanguage::read(pool, local_user.id).await?; // new user should be initialized with all languages assert_eq!(0, local_user_langs1.len()); // update user languages - let test_langs2 = test_langs2(pool).await; - LocalUserLanguage::update(pool, test_langs2, local_user.id) - .await - .unwrap(); - let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await.unwrap(); + let test_langs2 = test_langs2(pool).await?; + LocalUserLanguage::update(pool, test_langs2, local_user.id).await?; + let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await?; assert_eq!(3, local_user_langs2.len()); - Person::delete(pool, person.id).await.unwrap(); - LocalUser::delete(pool, local_user.id).await.unwrap(); - Site::delete(pool, site.id).await.unwrap(); - LocalSite::delete(pool).await.unwrap(); - Instance::delete(pool, instance.id).await.unwrap(); + Person::delete(pool, person.id).await?; + LocalUser::delete(pool, local_user.id).await?; + Site::delete(pool, site.id).await?; + LocalSite::delete(pool).await?; + Instance::delete(pool, instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_community_languages() { - let pool = &build_db_pool_for_tests().await; + async fn test_community_languages() -> Result<(), Error> { + 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; - SiteLanguage::update(pool, test_langs.clone(), &site) - .await - .unwrap(); + let (site, instance) = create_test_site(pool).await?; + let test_langs = test_langs1(pool).await?; + SiteLanguage::update(pool, test_langs.clone(), &site).await?; - let read_site_langs = SiteLanguage::read(pool, site.id).await.unwrap(); + let read_site_langs = SiteLanguage::read(pool, site.id).await?; assert_eq!(test_langs, read_site_langs); // Test the local ones are the same - let read_local_site_langs = SiteLanguage::read_local_raw(pool).await.unwrap(); + let read_local_site_langs = SiteLanguage::read_local_raw(pool).await?; assert_eq!(test_langs, read_local_site_langs); - let community_form = CommunityInsertForm::builder() - .name("test community".to_string()) - .title("test community".to_string()) - .public_key("pubkey".to_string()) - .instance_id(instance.id) - .build(); - let community = Community::create(pool, &community_form).await.unwrap(); - let community_langs1 = CommunityLanguage::read(pool, community.id).await.unwrap(); + let community_form = CommunityInsertForm::new( + instance.id, + "test community".to_string(), + "test community".to_string(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + let community_langs1 = CommunityLanguage::read(pool, community.id).await?; // community is initialized with site languages assert_eq!(test_langs, community_langs1); let allowed_lang1 = - CommunityLanguage::is_allowed_community_language(pool, Some(test_langs[0]), community.id) - .await; + CommunityLanguage::is_allowed_community_language(pool, test_langs[0], community.id).await; assert!(allowed_lang1.is_ok()); - let test_langs2 = test_langs2(pool).await; + let test_langs2 = test_langs2(pool).await?; let allowed_lang2 = - CommunityLanguage::is_allowed_community_language(pool, Some(test_langs2[0]), community.id) - .await; + CommunityLanguage::is_allowed_community_language(pool, test_langs2[0], community.id).await; assert!(allowed_lang2.is_err()); // limit site languages to en, fi. after this, community languages should be updated to // intersection of old languages (en, fr, ru) and (en, fi), which is only fi. - SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &site) - .await - .unwrap(); - let community_langs2 = CommunityLanguage::read(pool, community.id).await.unwrap(); + SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &site).await?; + let community_langs2 = CommunityLanguage::read(pool, community.id).await?; assert_eq!(vec![test_langs[0]], community_langs2); // update community languages to different ones - CommunityLanguage::update(pool, test_langs2.clone(), community.id) - .await - .unwrap(); - let community_langs3 = CommunityLanguage::read(pool, community.id).await.unwrap(); + CommunityLanguage::update(pool, test_langs2.clone(), community.id).await?; + let community_langs3 = CommunityLanguage::read(pool, community.id).await?; assert_eq!(test_langs2, community_langs3); - Community::delete(pool, community.id).await.unwrap(); - Site::delete(pool, site.id).await.unwrap(); - LocalSite::delete(pool).await.unwrap(); - Instance::delete(pool, instance.id).await.unwrap(); + Community::delete(pool, community.id).await?; + Site::delete(pool, site.id).await?; + LocalSite::delete(pool).await?; + Instance::delete(pool, instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_default_post_language() { - let pool = &build_db_pool_for_tests().await; + async fn test_default_post_language() -> Result<(), Error> { + 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; - let test_langs2 = test_langs2(pool).await; + let (site, instance) = create_test_site(pool).await?; + let test_langs = test_langs1(pool).await?; + let test_langs2 = test_langs2(pool).await?; - let community_form = CommunityInsertForm::builder() - .name("test community".to_string()) - .title("test community".to_string()) - .public_key("pubkey".to_string()) - .instance_id(instance.id) - .build(); - let community = Community::create(pool, &community_form).await.unwrap(); - CommunityLanguage::update(pool, test_langs, community.id) - .await - .unwrap(); + let community_form = CommunityInsertForm::new( + instance.id, + "test community".to_string(), + "test community".to_string(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + CommunityLanguage::update(pool, test_langs, community.id).await?; let person_form = PersonInsertForm::test_form(instance.id, "my test person"); - let person = Person::create(pool, &person_form).await.unwrap(); + let person = Person::create(pool, &person_form).await?; let local_user_form = LocalUserInsertForm::test_form(person.id); - let local_user = LocalUser::create(pool, &local_user_form, vec![]) - .await - .unwrap(); - LocalUserLanguage::update(pool, test_langs2, local_user.id) - .await - .unwrap(); + let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; + LocalUserLanguage::update(pool, test_langs2, local_user.id).await?; // no overlap in user/community languages, so defaults to undetermined - let def1 = default_post_language(pool, community.id, local_user.id) - .await - .unwrap(); - assert_eq!(None, def1); + let def1 = default_post_language(pool, community.id, local_user.id).await?; + assert_eq!(UNDETERMINED_ID, def1); - let ru = Language::read_id_from_code(pool, Some("ru")) - .await - .unwrap() - .unwrap(); + let ru = Language::read_id_from_code(pool, "ru").await?; let test_langs3 = vec![ ru, - Language::read_id_from_code(pool, Some("fi")) - .await - .unwrap() - .unwrap(), - Language::read_id_from_code(pool, Some("se")) - .await - .unwrap() - .unwrap(), + Language::read_id_from_code(pool, "fi").await?, + Language::read_id_from_code(pool, "se").await?, UNDETERMINED_ID, ]; - LocalUserLanguage::update(pool, test_langs3, local_user.id) - .await - .unwrap(); + LocalUserLanguage::update(pool, test_langs3, local_user.id).await?; // this time, both have ru as common lang - let def2 = default_post_language(pool, community.id, local_user.id) - .await - .unwrap(); - assert_eq!(Some(ru), def2); + let def2 = default_post_language(pool, community.id, local_user.id).await?; + assert_eq!(ru, def2); - Person::delete(pool, person.id).await.unwrap(); - Community::delete(pool, community.id).await.unwrap(); - LocalUser::delete(pool, local_user.id).await.unwrap(); - Site::delete(pool, site.id).await.unwrap(); - LocalSite::delete(pool).await.unwrap(); - Instance::delete(pool, instance.id).await.unwrap(); + Person::delete(pool, person.id).await?; + Community::delete(pool, community.id).await?; + LocalUser::delete(pool, local_user.id).await?; + Site::delete(pool, site.id).await?; + LocalSite::delete(pool).await?; + Instance::delete(pool, instance.id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/impls/captcha_answer.rs b/crates/db_schema/src/impls/captcha_answer.rs index 1d0604c0a..8be8fc5de 100644 --- a/crates/db_schema/src/impls/captcha_answer.rs +++ b/crates/db_schema/src/impls/captcha_answer.rs @@ -13,6 +13,7 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CaptchaAnswer { pub async fn insert(pool: &mut DbPool<'_>, captcha: &CaptchaAnswerForm) -> Result { @@ -27,7 +28,7 @@ impl CaptchaAnswer { pub async fn check_captcha( pool: &mut DbPool<'_>, to_check: CheckCaptchaAnswer, - ) -> Result { + ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; // fetch requested captcha @@ -43,13 +44,13 @@ impl CaptchaAnswer { .execute(conn) .await?; - Ok(captcha_exists) + captcha_exists + .then_some(()) + .ok_or(LemmyErrorType::CaptchaIncorrect.into()) } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -61,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( @@ -83,13 +84,12 @@ mod tests { .await; assert!(result.is_ok()); - assert!(result.unwrap()); } #[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( @@ -119,7 +119,6 @@ mod tests { ) .await; - assert!(result_repeat.is_ok()); - assert!(!result_repeat.unwrap()); + assert!(result_repeat.is_err()); } } diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 977bc9083..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; @@ -40,12 +55,12 @@ impl Comment { pub async fn update_removed_for_creator( pool: &mut DbPool<'_>, for_creator_id: PersonId, - new_removed: bool, + removed: bool, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; diesel::update(comment::table.filter(comment::creator_id.eq(for_creator_id))) .set(( - comment::removed.eq(new_removed), + comment::removed.eq(removed), comment::updated.eq(naive_now()), )) .get_results::(conn) @@ -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,33 +191,35 @@ 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 } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -218,51 +240,47 @@ 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; use pretty_assertions::assert_eq; use serial_test::serial; use url::Url; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> LemmyResult<()> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "terry"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; - let new_community = CommunityInsertForm::builder() - .name("test community".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; let expected_comment = Comment { id: inserted_comment.id, @@ -277,38 +295,32 @@ mod tests { ap_id: Url::parse(&format!( "https://lemmy-alpha/comment/{}", inserted_comment.id - )) - .unwrap() + ))? .into(), distinguished: false, local: true, language_id: LanguageId::default(), }; - let child_comment_form = CommentInsertForm::builder() - .content("A child comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A child comment".into(), + ); let inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; // Comment Like let comment_like_form = CommentLikeForm { comment_id: inserted_comment.id, - post_id: inserted_post.id, person_id: inserted_person.id, score: 1, }; - let inserted_comment_like = CommentLike::like(pool, &comment_like_form).await.unwrap(); + let inserted_comment_like = CommentLike::like(pool, &comment_like_form).await?; 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, @@ -320,7 +332,7 @@ mod tests { person_id: inserted_person.id, }; - let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await.unwrap(); + let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await?; let expected_comment_saved = CommentSaved { comment_id: inserted_comment.id, @@ -333,30 +345,17 @@ mod tests { ..Default::default() }; - let updated_comment = Comment::update(pool, inserted_comment.id, &comment_update_form) - .await - .unwrap(); + let updated_comment = Comment::update(pool, inserted_comment.id, &comment_update_form).await?; - let read_comment = Comment::read(pool, inserted_comment.id) - .await - .unwrap() - .unwrap(); - let like_removed = CommentLike::remove(pool, inserted_person.id, inserted_comment.id) - .await - .unwrap(); - let saved_removed = CommentSaved::unsave(pool, &comment_saved_form) - .await - .unwrap(); - let num_deleted = Comment::delete(pool, inserted_comment.id).await.unwrap(); - Comment::delete(pool, inserted_child_comment.id) - .await - .unwrap(); - Post::delete(pool, inserted_post.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let read_comment = Comment::read(pool, inserted_comment.id).await?; + let like_removed = CommentLike::remove(pool, inserted_person.id, inserted_comment.id).await?; + let saved_removed = CommentSaved::unsave(pool, &comment_saved_form).await?; + let num_deleted = Comment::delete(pool, inserted_comment.id).await?; + Comment::delete(pool, inserted_child_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_comment, read_comment); assert_eq!(expected_comment, inserted_comment); @@ -367,8 +366,10 @@ 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 eaf35a90d..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,17 +20,22 @@ use crate::{ }, traits::{ApubActor, Bannable, Crud, Followable, Joinable}, utils::{ + action_query, + find_action, functions::{coalesce, lower}, get_conn, + now, + uplete, DbPool, }, + ListingType, SubscribedType, }; use chrono::{DateTime, Utc}; use diesel::{ deserialize, - dsl, - dsl::{exists, insert_into}, + 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 } } @@ -152,26 +163,24 @@ impl Community { pub async fn get_by_collection_url( pool: &mut DbPool<'_>, url: &DbUrl, - ) -> Result, Error> { + ) -> LemmyResult<(Community, CollectionType)> { let conn = &mut get_conn(pool).await?; let res = community::table .filter(community::moderators_url.eq(url)) .first(conn) - .await - .optional()?; + .await; - if let Some(c) = res { - Ok(Some((c, CollectionType::Moderators))) + if let Ok(c) = res { + Ok((c, CollectionType::Moderators)) } else { let res = community::table .filter(community::featured_url.eq(url)) .first(conn) - .await - .optional()?; - if let Some(c) = res { - Ok(Some((c, CollectionType::Featured))) + .await; + if let Ok(c) = res { + Ok((c, CollectionType::Featured)) } else { - Ok(None) + Err(LemmyErrorType::NotFound.into()) } } } @@ -196,32 +205,56 @@ impl Community { .await?; Ok(()) } + + pub async fn get_random_community_id( + pool: &mut DbPool<'_>, + type_: &Option, + ) -> Result { + let conn = &mut get_conn(pool).await?; + sql_function!(fn random() -> Text); + + let mut query = community::table + .filter(not(community::deleted)) + .filter(not(community::removed)) + .into_boxed(); + + if let Some(ListingType::Local) = type_ { + query = query.filter(community::local); + } + + query + .select(community::id) + .order(random()) + .limit(1) + .first::(conn) + .await + } } 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( @@ -229,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 } @@ -250,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)? @@ -275,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 } @@ -290,57 +329,71 @@ 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) - .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() - } - - /// Check if a remote instance has any followers on local instance. For this it is enough to check - /// if any follow relation is stored. Dont use this for local community. - pub async fn has_local_followers( - pool: &mut DbPool<'_>, - remote_community_id: CommunityId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - select(exists(community_follower::table.filter( - community_follower::community_id.eq(remote_community_id), - ))) + .set_null(community_actions::received_ban) + .set_null(community_actions::ban_expires) .get_result(conn) .await } } -impl Queryable, Pg> for SubscribedType { - type Row = Option; +impl CommunityFollower { + 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 + /// if any follow relation is stored. Dont use this for local community. + pub async fn check_has_local_followers( + pool: &mut DbPool<'_>, + remote_community_id: CommunityId, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + 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; 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, }) } @@ -351,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 } @@ -368,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 } } @@ -432,7 +496,6 @@ impl ApubActor for Community { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ source::{ @@ -440,6 +503,7 @@ mod tests { Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -452,17 +516,17 @@ 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, LemmyErrorType}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[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?; @@ -473,19 +537,19 @@ mod tests { let artemis_person = PersonInsertForm::test_form(inserted_instance.id, "artemis"); let inserted_artemis = Person::create(pool, &artemis_person).await?; - let new_community = CommunityInsertForm::builder() - .name("TIL".into()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); let inserted_community = Community::create(pool, &new_community).await?; let expected_community = Community { id: inserted_community.id, name: "TIL".into(), title: "nada".to_owned(), + sidebar: None, description: None, nsfw: false, removed: false, @@ -501,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, @@ -513,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 = @@ -522,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 { @@ -595,9 +660,7 @@ mod tests { expires: None, }; - let read_community = Community::read(pool, inserted_community.id) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let read_community = Community::read(pool, inserted_community.id).await?; let update_community_form = CommunityUpdateForm { title: Some("nada".to_owned()), @@ -620,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 1393f49d3..c520e43e8 100644 --- a/crates/db_schema/src/impls/community_block.rs +++ b/crates/db_schema/src/impls/community_block.rs @@ -1,30 +1,56 @@ use crate::{ newtypes::{CommunityId, PersonId}, - schema::community_block::dsl::{community_block, community_id, person_id}, - source::community_block::{CommunityBlock, CommunityBlockForm}, + 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}, + dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, + ExpressionMethods, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CommunityBlock { pub async fn read( pool: &mut DbPool<'_>, for_person_id: PersonId, for_community_id: CommunityId, - ) -> Result { + ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(exists( - community_block.find((for_person_id, for_community_id)), - )) - .get_result(conn) - .await + select(not(exists(find_action( + community_actions::blocked, + (for_person_id, for_community_id), + )))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::CommunityIsBlocked.into()) + } + + pub async fn for_person( + pool: &mut DbPool<'_>, + person_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + action_query(community_actions::blocked) + .inner_join(community::table) + .select(community::all_columns) + .filter(community_actions::person_id.eq(person_id)) + .filter(community::deleted.eq(false)) + .filter(community::removed.eq(false)) + .order_by(community_actions::blocked) + .load::(conn) + .await } } @@ -33,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) + let community_block_form = ( + community_block_form, + community_actions::blocked.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_block_form) - .on_conflict((person_id, 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.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/custom_emoji.rs b/crates/db_schema/src/impls/custom_emoji.rs index 050301659..9ba359071 100644 --- a/crates/db_schema/src/impls/custom_emoji.rs +++ b/crates/db_schema/src/impls/custom_emoji.rs @@ -8,36 +8,37 @@ use crate::{ custom_emoji::{CustomEmoji, CustomEmojiInsertForm, CustomEmojiUpdateForm}, custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, }, + traits::Crud, utils::{get_conn, DbPool}, }; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -impl CustomEmoji { - pub async fn create(pool: &mut DbPool<'_>, form: &CustomEmojiInsertForm) -> Result { +#[async_trait] +impl Crud for CustomEmoji { + type InsertForm = CustomEmojiInsertForm; + type UpdateForm = CustomEmojiUpdateForm; + type IdType = CustomEmojiId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; insert_into(custom_emoji) .values(form) .get_result::(conn) .await } - pub async fn update( + + async fn update( pool: &mut DbPool<'_>, - emoji_id: CustomEmojiId, - form: &CustomEmojiUpdateForm, + emoji_id: Self::IdType, + new_custom_emoji: &Self::UpdateForm, ) -> Result { let conn = &mut get_conn(pool).await?; diesel::update(custom_emoji.find(emoji_id)) - .set(form) + .set(new_custom_emoji) .get_result::(conn) .await } - pub async fn delete(pool: &mut DbPool<'_>, emoji_id: CustomEmojiId) -> Result { - let conn = &mut get_conn(pool).await?; - diesel::delete(custom_emoji.find(emoji_id)) - .execute(conn) - .await - } } impl CustomEmojiKeyword { diff --git a/crates/db_schema/src/impls/email_verification.rs b/crates/db_schema/src/impls/email_verification.rs index b4951cf73..39c7fe0bc 100644 --- a/crates/db_schema/src/impls/email_verification.rs +++ b/crates/db_schema/src/impls/email_verification.rs @@ -16,7 +16,6 @@ use diesel::{ sql_types::Timestamptz, ExpressionMethods, IntoSql, - OptionalExtension, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -30,14 +29,13 @@ impl EmailVerification { .await } - pub async fn read_for_token(pool: &mut DbPool<'_>, token: &str) -> Result, Error> { + pub async fn read_for_token(pool: &mut DbPool<'_>, token: &str) -> Result { let conn = &mut get_conn(pool).await?; email_verification .filter(verification_token.eq(token)) .filter(published.gt(now.into_sql::() - 7.days())) .first(conn) .await - .optional() } pub async fn delete_old_tokens_for_local_user( pool: &mut DbPool<'_>, diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index 80408a526..099e0b231 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -48,21 +48,20 @@ impl FederationAllowList { } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ source::{federation_allowlist::FederationAllowList, instance::Instance}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_allowlist_insert_and_clear() { - let pool = &build_db_pool_for_tests().await; + async fn test_allowlist_insert_and_clear() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let domains = vec![ "tld1.xyz".to_string(), @@ -72,9 +71,9 @@ mod tests { let allowed = Some(domains.clone()); - FederationAllowList::replace(pool, allowed).await.unwrap(); + FederationAllowList::replace(pool, allowed).await?; - let allows = Instance::allowlist(pool).await.unwrap(); + let allows = Instance::allowlist(pool).await?; let allows_domains = allows .iter() .map(|i| i.domain.clone()) @@ -86,13 +85,13 @@ mod tests { // Now test clearing them via Some(empty vec) let clear_allows = Some(Vec::new()); - FederationAllowList::replace(pool, clear_allows) - .await - .unwrap(); - let allows = Instance::allowlist(pool).await.unwrap(); + FederationAllowList::replace(pool, clear_allows).await?; + let allows = Instance::allowlist(pool).await?; assert_eq!(0, allows.len()); - Instance::delete_all(pool).await.unwrap(); + Instance::delete_all(pool).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/impls/images.rs b/crates/db_schema/src/impls/images.rs index 547bfc4e2..8ded98e41 100644 --- a/crates/db_schema/src/impls/images.rs +++ b/crates/db_schema/src/impls/images.rs @@ -1,14 +1,7 @@ use crate::{ newtypes::DbUrl, schema::{image_details, local_image, remote_image}, - source::images::{ - ImageDetails, - ImageDetailsForm, - LocalImage, - LocalImageForm, - RemoteImage, - RemoteImageForm, - }, + source::images::{ImageDetails, ImageDetailsForm, LocalImage, LocalImageForm, RemoteImage}, utils::{get_conn, DbPool}, }; use diesel::{ @@ -20,7 +13,8 @@ use diesel::{ NotFound, QueryDsl, }; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel_async::RunQueryDsl; +use url::Url; impl LocalImage { pub async fn create( @@ -38,7 +32,7 @@ impl LocalImage { .get_result::(conn) .await; - ImageDetails::create(conn, image_details_form).await?; + ImageDetails::create(&mut conn.into(), image_details_form).await?; local_insert }) as _ @@ -60,26 +54,16 @@ impl LocalImage { } impl RemoteImage { - pub async fn create(pool: &mut DbPool<'_>, form: &ImageDetailsForm) -> Result { + pub async fn create(pool: &mut DbPool<'_>, links: Vec) -> Result { let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - let remote_image_form = RemoteImageForm { - link: form.link.clone(), - }; - let remote_insert = insert_into(remote_image::table) - .values(remote_image_form) - .on_conflict_do_nothing() - .execute(conn) - .await; - - ImageDetails::create(conn, form).await?; - - remote_insert - }) as _ - }) + let forms = links + .into_iter() + .map(|url| remote_image::dsl::link.eq::(url.into())) + .collect::>(); + insert_into(remote_image::table) + .values(forms) + .on_conflict_do_nothing() + .execute(conn) .await } @@ -100,10 +84,9 @@ impl RemoteImage { } impl ImageDetails { - pub(crate) async fn create( - conn: &mut AsyncPgConnection, - form: &ImageDetailsForm, - ) -> Result { + pub async fn create(pool: &mut DbPool<'_>, form: &ImageDetailsForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(image_details::table) .values(form) .on_conflict_do_nothing() diff --git a/crates/db_schema/src/impls/instance.rs b/crates/db_schema/src/impls/instance.rs index 94bf909a3..6c72b5e18 100644 --- a/crates/db_schema/src/impls/instance.rs +++ b/crates/db_schema/src/impls/instance.rs @@ -51,10 +51,10 @@ impl Instance { Some(i) => Ok(i), None => { // Instance not in database yet, insert it - let form = InstanceForm::builder() - .domain(domain_) - .updated(Some(naive_now())) - .build(); + let form = InstanceForm { + updated: Some(naive_now()), + ..InstanceForm::new(domain_) + }; insert_into(instance::table) .values(&form) // Necessary because this method may be called concurrently for the same domain. This @@ -67,6 +67,11 @@ impl Instance { } } } + pub async fn read(pool: &mut DbPool<'_>, instance_id: InstanceId) -> Result { + let conn = &mut get_conn(pool).await?; + instance::table.find(instance_id).first(conn).await + } + pub async fn update( pool: &mut DbPool<'_>, instance_id: InstanceId, diff --git a/crates/db_schema/src/impls/instance_block.rs b/crates/db_schema/src/impls/instance_block.rs index e32688411..1722e8318 100644 --- a/crates/db_schema/src/impls/instance_block.rs +++ b/crates/db_schema/src/impls/instance_block.rs @@ -1,30 +1,54 @@ use crate::{ newtypes::{InstanceId, PersonId}, - schema::instance_block::dsl::{instance_block, instance_id, person_id}, - source::instance_block::{InstanceBlock, InstanceBlockForm}, + 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}, + dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, + ExpressionMethods, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl InstanceBlock { pub async fn read( pool: &mut DbPool<'_>, for_person_id: PersonId, for_instance_id: InstanceId, - ) -> Result { + ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(exists( - instance_block.find((for_person_id, for_instance_id)), - )) - .get_result(conn) - .await + select(not(exists(find_action( + instance_actions::blocked, + (for_person_id, for_instance_id), + )))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::InstanceIsBlocked.into()) + } + + pub async fn for_person( + pool: &mut DbPool<'_>, + person_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + action_query(instance_actions::blocked) + .inner_join(instance::table) + .select(instance::all_columns) + .filter(instance_actions::person_id.eq(person_id)) + .order_by(instance_actions::blocked) + .load::(conn) + .await } } @@ -33,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) + let instance_block_form = ( + instance_block_form, + instance_actions::blocked.eq(now().nullable()), + ); + insert_into(instance_actions::table) .values(instance_block_form) - .on_conflict((person_id, 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.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 6a7b4e9ac..3b8bc1d20 100644 --- a/crates/db_schema/src/impls/language.rs +++ b/crates/db_schema/src/impls/language.rs @@ -1,3 +1,4 @@ +use super::actor_language::UNDETERMINED_ID; use crate::{ diesel::ExpressionMethods, newtypes::LanguageId, @@ -19,47 +20,42 @@ impl Language { language::table.find(id_).first(conn).await } - /// Attempts to find the given language code and return its ID. If not found, returns none. - pub async fn read_id_from_code( - pool: &mut DbPool<'_>, - code_: Option<&str>, - ) -> Result, Error> { - if let Some(code_) = code_ { - let conn = &mut get_conn(pool).await?; - Ok( - language::table - .filter(language::code.eq(code_)) - .first::(conn) - .await - .map(|l| l.id) - .ok(), - ) - } else { - Ok(None) - } + /// Attempts to find the given language code and return its ID. + pub async fn read_id_from_code(pool: &mut DbPool<'_>, code_: &str) -> Result { + let conn = &mut get_conn(pool).await?; + let res = language::table + .filter(language::code.eq(code_)) + .first::(conn) + .await + .map(|l| l.id); + + // Return undetermined by default + Ok(res.unwrap_or(UNDETERMINED_ID)) } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{source::language::Language, utils::build_db_pool_for_tests}; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_languages() { - let pool = &build_db_pool_for_tests().await; + async fn test_languages() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let all = Language::read_all(pool).await.unwrap(); + let all = Language::read_all(pool).await?; assert_eq!(184, all.len()); assert_eq!("ak", all[5].code); assert_eq!("lv", all[99].code); assert_eq!("yi", all[179].code); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index acff6af2a..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, @@ -35,9 +36,11 @@ impl LocalUser { ) -> Result { let conn = &mut get_conn(pool).await?; let mut form_with_encrypted_password = form.clone(); - let password_hash = - hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); - form_with_encrypted_password.password_encrypted = password_hash; + + if let Some(password_encrypted) = &form.password_encrypted { + let password_hash = hash(password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); + form_with_encrypted_password.password_encrypted = Some(password_hash); + } let local_user_ = insert_into(local_user::table) .values(form_with_encrypted_password) @@ -47,9 +50,7 @@ impl LocalUser { LocalUserLanguage::update(pool, languages, local_user_.id).await?; // Create their vote_display_modes - let vote_display_mode_form = LocalUserVoteDisplayModeInsertForm::builder() - .local_user_id(local_user_.id) - .build(); + let vote_display_mode_form = LocalUserVoteDisplayModeInsertForm::new(local_user_.id); LocalUserVoteDisplayMode::create(pool, &vote_display_mode_form).await?; Ok(local_user_) @@ -136,14 +137,16 @@ impl LocalUser { diesel::delete(persons).execute(conn).await } - pub async fn is_email_taken(pool: &mut DbPool<'_>, email: &str) -> Result { + pub async fn check_is_email_taken(pool: &mut DbPool<'_>, email: &str) -> LemmyResult<()> { use diesel::dsl::{exists, select}; let conn = &mut get_conn(pool).await?; - select(exists(local_user::table.filter( + select(not(exists(local_user::table.filter( lower(coalesce(local_user::email, "")).eq(email.to_lowercase()), - ))) - .get_result(conn) - .await + )))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::EmailAlreadyExists.into()) } // TODO: maybe move this and pass in LocalUserView @@ -153,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) @@ -268,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)?; @@ -329,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< @@ -346,7 +349,7 @@ impl LocalUserOptionHelper for Option<&LocalUser> { impl LocalUserInsertForm { pub fn test_form(person_id: PersonId) -> Self { - Self::new(person_id, String::new()) + Self::new(person_id, Some(String::new())) } pub fn test_form_admin(person_id: PersonId) -> Self { @@ -367,7 +370,6 @@ pub struct UserBackupLists { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ source::{ @@ -384,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?; @@ -419,4 +421,32 @@ mod tests { Ok(()) } + + #[tokio::test] + #[serial] + async fn test_email_taken() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let darwin_email = "charles.darwin@gmail.com"; + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let darwin_person = PersonInsertForm::test_form(inserted_instance.id, "darwin"); + let inserted_darwin_person = Person::create(pool, &darwin_person).await?; + + let mut darwin_local_user_form = + LocalUserInsertForm::test_form_admin(inserted_darwin_person.id); + darwin_local_user_form.email = Some(darwin_email.into()); + let _inserted_darwin_local_user = + LocalUser::create(pool, &darwin_local_user_form, vec![]).await?; + + let check = LocalUser::check_is_email_taken(pool, darwin_email).await; + assert!(check.is_err()); + + let passed_check = LocalUser::check_is_email_taken(pool, "not_charles@gmail.com").await; + assert!(passed_check.is_ok()); + + Ok(()) + } } diff --git a/crates/db_schema/src/impls/login_token.rs b/crates/db_schema/src/impls/login_token.rs index 71cac6a19..f4f7a7aae 100644 --- a/crates/db_schema/src/impls/login_token.rs +++ b/crates/db_schema/src/impls/login_token.rs @@ -7,6 +7,7 @@ use crate::{ }; use diesel::{delete, dsl::exists, insert_into, result::Error, select}; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl LoginToken { pub async fn create(pool: &mut DbPool<'_>, form: LoginTokenCreateForm) -> Result { @@ -22,13 +23,15 @@ impl LoginToken { pool: &mut DbPool<'_>, user_id_: LocalUserId, token_: &str, - ) -> Result { + ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; select(exists( login_token.find(token_).filter(user_id.eq(user_id_)), )) - .get_result(conn) - .await + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::NotLoggedIn.into()) } pub async fn list( diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 3a4e71307..f115a101f 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -22,6 +22,8 @@ pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; pub mod moderator; +pub mod oauth_account; +pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/moderator.rs index c10d818f8..8deb56258 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/moderator.rs @@ -66,6 +66,20 @@ impl Crud for ModRemovePost { } } +impl ModRemovePost { + pub async fn create_multiple( + pool: &mut DbPool<'_>, + forms: &Vec, + ) -> Result { + use crate::schema::mod_remove_post::dsl::mod_remove_post; + let conn = &mut get_conn(pool).await?; + insert_into(mod_remove_post) + .values(forms) + .execute(conn) + .await + } +} + #[async_trait] impl Crud for ModLockPost { type InsertForm = ModLockPostForm; @@ -153,6 +167,20 @@ impl Crud for ModRemoveComment { } } +impl ModRemoveComment { + pub async fn create_multiple( + pool: &mut DbPool<'_>, + forms: &Vec, + ) -> Result { + use crate::schema::mod_remove_comment::dsl::mod_remove_comment; + let conn = &mut get_conn(pool).await?; + insert_into(mod_remove_comment) + .values(forms) + .execute(conn) + .await + } +} + #[async_trait] impl Crud for ModRemoveCommunity { type InsertForm = ModRemoveCommunityForm; @@ -465,8 +493,6 @@ impl Crud for AdminPurgeComment { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -500,51 +526,48 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> Result<(), Error> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_mod = PersonInsertForm::test_form(inserted_instance.id, "the mod"); - let inserted_mod = Person::create(pool, &new_mod).await.unwrap(); + let inserted_mod = Person::create(pool, &new_mod).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "jim2"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; - let new_community = CommunityInsertForm::builder() - .name("mod_community".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "mod_community".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; - let new_post = PostInsertForm::builder() - .name("A test post thweep".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); + let new_post = PostInsertForm::new( + "A test post thweep".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; // Now the actual tests @@ -555,13 +578,8 @@ mod tests { reason: None, removed: None, }; - let inserted_mod_remove_post = ModRemovePost::create(pool, &mod_remove_post_form) - .await - .unwrap(); - let read_mod_remove_post = ModRemovePost::read(pool, inserted_mod_remove_post.id) - .await - .unwrap() - .unwrap(); + let inserted_mod_remove_post = ModRemovePost::create(pool, &mod_remove_post_form).await?; + let read_mod_remove_post = ModRemovePost::read(pool, inserted_mod_remove_post.id).await?; let expected_mod_remove_post = ModRemovePost { id: inserted_mod_remove_post.id, post_id: inserted_post.id, @@ -578,13 +596,8 @@ mod tests { post_id: inserted_post.id, locked: None, }; - let inserted_mod_lock_post = ModLockPost::create(pool, &mod_lock_post_form) - .await - .unwrap(); - let read_mod_lock_post = ModLockPost::read(pool, inserted_mod_lock_post.id) - .await - .unwrap() - .unwrap(); + let inserted_mod_lock_post = ModLockPost::create(pool, &mod_lock_post_form).await?; + let read_mod_lock_post = ModLockPost::read(pool, inserted_mod_lock_post.id).await?; let expected_mod_lock_post = ModLockPost { id: inserted_mod_lock_post.id, post_id: inserted_post.id, @@ -601,13 +614,8 @@ mod tests { featured: false, is_featured_community: true, }; - let inserted_mod_feature_post = ModFeaturePost::create(pool, &mod_feature_post_form) - .await - .unwrap(); - let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id) - .await - .unwrap() - .unwrap(); + let inserted_mod_feature_post = ModFeaturePost::create(pool, &mod_feature_post_form).await?; + let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id).await?; let expected_mod_feature_post = ModFeaturePost { id: inserted_mod_feature_post.id, post_id: inserted_post.id, @@ -625,13 +633,10 @@ mod tests { reason: None, removed: None, }; - let inserted_mod_remove_comment = ModRemoveComment::create(pool, &mod_remove_comment_form) - .await - .unwrap(); - let read_mod_remove_comment = ModRemoveComment::read(pool, inserted_mod_remove_comment.id) - .await - .unwrap() - .unwrap(); + let inserted_mod_remove_comment = + ModRemoveComment::create(pool, &mod_remove_comment_form).await?; + let read_mod_remove_comment = + ModRemoveComment::read(pool, inserted_mod_remove_comment.id).await?; let expected_mod_remove_comment = ModRemoveComment { id: inserted_mod_remove_comment.id, comment_id: inserted_comment.id, @@ -650,14 +655,9 @@ mod tests { removed: None, }; let inserted_mod_remove_community = - ModRemoveCommunity::create(pool, &mod_remove_community_form) - .await - .unwrap(); + ModRemoveCommunity::create(pool, &mod_remove_community_form).await?; let read_mod_remove_community = - ModRemoveCommunity::read(pool, inserted_mod_remove_community.id) - .await - .unwrap() - .unwrap(); + ModRemoveCommunity::read(pool, inserted_mod_remove_community.id).await?; let expected_mod_remove_community = ModRemoveCommunity { id: inserted_mod_remove_community.id, community_id: inserted_community.id, @@ -678,14 +678,9 @@ mod tests { expires: None, }; let inserted_mod_ban_from_community = - ModBanFromCommunity::create(pool, &mod_ban_from_community_form) - .await - .unwrap(); + ModBanFromCommunity::create(pool, &mod_ban_from_community_form).await?; let read_mod_ban_from_community = - ModBanFromCommunity::read(pool, inserted_mod_ban_from_community.id) - .await - .unwrap() - .unwrap(); + ModBanFromCommunity::read(pool, inserted_mod_ban_from_community.id).await?; let expected_mod_ban_from_community = ModBanFromCommunity { id: inserted_mod_ban_from_community.id, community_id: inserted_community.id, @@ -706,11 +701,8 @@ mod tests { banned: None, expires: None, }; - let inserted_mod_ban = ModBan::create(pool, &mod_ban_form).await.unwrap(); - let read_mod_ban = ModBan::read(pool, inserted_mod_ban.id) - .await - .unwrap() - .unwrap(); + let inserted_mod_ban = ModBan::create(pool, &mod_ban_form).await?; + let read_mod_ban = ModBan::read(pool, inserted_mod_ban.id).await?; let expected_mod_ban = ModBan { id: inserted_mod_ban.id, mod_person_id: inserted_mod.id, @@ -729,13 +721,8 @@ mod tests { community_id: inserted_community.id, removed: None, }; - let inserted_mod_add_community = ModAddCommunity::create(pool, &mod_add_community_form) - .await - .unwrap(); - let read_mod_add_community = ModAddCommunity::read(pool, inserted_mod_add_community.id) - .await - .unwrap() - .unwrap(); + let inserted_mod_add_community = ModAddCommunity::create(pool, &mod_add_community_form).await?; + let read_mod_add_community = ModAddCommunity::read(pool, inserted_mod_add_community.id).await?; let expected_mod_add_community = ModAddCommunity { id: inserted_mod_add_community.id, community_id: inserted_community.id, @@ -752,11 +739,8 @@ mod tests { other_person_id: inserted_person.id, removed: None, }; - let inserted_mod_add = ModAdd::create(pool, &mod_add_form).await.unwrap(); - let read_mod_add = ModAdd::read(pool, inserted_mod_add.id) - .await - .unwrap() - .unwrap(); + let inserted_mod_add = ModAdd::create(pool, &mod_add_form).await?; + let read_mod_add = ModAdd::read(pool, inserted_mod_add.id).await?; let expected_mod_add = ModAdd { id: inserted_mod_add.id, mod_person_id: inserted_mod.id, @@ -765,14 +749,12 @@ mod tests { when_: inserted_mod_add.when_, }; - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Post::delete(pool, inserted_post.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Person::delete(pool, inserted_mod.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Comment::delete(pool, inserted_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_person.id).await?; + Person::delete(pool, inserted_mod.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_mod_remove_post, read_mod_remove_post); assert_eq!(expected_mod_lock_post, read_mod_lock_post); @@ -783,5 +765,7 @@ mod tests { assert_eq!(expected_mod_ban, read_mod_ban); assert_eq!(expected_mod_add_community, read_mod_add_community); assert_eq!(expected_mod_add, read_mod_add); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/oauth_account.rs b/crates/db_schema/src/impls/oauth_account.rs new file mode 100644 index 000000000..7210b7a37 --- /dev/null +++ b/crates/db_schema/src/impls/oauth_account.rs @@ -0,0 +1,29 @@ +use crate::{ + newtypes::LocalUserId, + schema::{oauth_account, oauth_account::dsl::local_user_id}, + source::oauth_account::{OAuthAccount, OAuthAccountInsertForm}, + utils::{get_conn, DbPool}, +}; +use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; + +impl OAuthAccount { + pub async fn create(pool: &mut DbPool<'_>, form: &OAuthAccountInsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(oauth_account::table) + .values(form) + .get_result::(conn) + .await + } + + pub async fn delete_user_accounts( + pool: &mut DbPool<'_>, + for_local_user_id: LocalUserId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + diesel::delete(oauth_account::table.filter(local_user_id.eq(for_local_user_id))) + .execute(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/oauth_provider.rs b/crates/db_schema/src/impls/oauth_provider.rs new file mode 100644 index 000000000..9d7e791e7 --- /dev/null +++ b/crates/db_schema/src/impls/oauth_provider.rs @@ -0,0 +1,71 @@ +use crate::{ + newtypes::OAuthProviderId, + schema::oauth_provider, + source::oauth_provider::{ + OAuthProvider, + OAuthProviderInsertForm, + OAuthProviderUpdateForm, + PublicOAuthProvider, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for OAuthProvider { + type InsertForm = OAuthProviderInsertForm; + type UpdateForm = OAuthProviderUpdateForm; + type IdType = OAuthProviderId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(oauth_provider::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + oauth_provider_id: OAuthProviderId, + form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(oauth_provider::table.find(oauth_provider_id)) + .set(form) + .get_result::(conn) + .await + } +} + +impl OAuthProvider { + pub async fn get_all(pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let oauth_providers = oauth_provider::table + .order(oauth_provider::id) + .select(oauth_provider::all_columns) + .load::(conn) + .await?; + + Ok(oauth_providers) + } + + pub fn convert_providers_to_public( + oauth_providers: Vec, + ) -> Vec { + let mut result = Vec::::new(); + for oauth_provider in &oauth_providers { + if oauth_provider.enabled { + result.push(PublicOAuthProvider(oauth_provider.clone())); + } + } + result + } + + pub async fn get_all_public(pool: &mut DbPool<'_>) -> Result, Error> { + let oauth_providers = OAuthProvider::get_all(pool).await?; + Ok(Self::convert_providers_to_public(oauth_providers)) + } +} diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index be05ed8ac..a9ac3a9c2 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -1,5 +1,4 @@ use crate::{ - diesel::OptionalExtension, newtypes::LocalUserId, schema::password_reset_request::dsl::{password_reset_request, published, token}, source::password_reset_request::{PasswordResetRequest, PasswordResetRequestForm}, @@ -32,20 +31,17 @@ impl PasswordResetRequest { .await } - pub async fn read_and_delete(pool: &mut DbPool<'_>, token_: &str) -> Result, Error> { + pub async fn read_and_delete(pool: &mut DbPool<'_>, token_: &str) -> Result { let conn = &mut get_conn(pool).await?; delete(password_reset_request) .filter(token.eq(token_)) .filter(published.gt(now.into_sql::() - 1.days())) .get_result(conn) .await - .optional() } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -65,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 @@ -81,9 +77,7 @@ mod tests { PasswordResetRequest::create(pool, inserted_local_user.id, token.to_string()).await?; // Read it and verify - let read_password_reset_request = PasswordResetRequest::read_and_delete(pool, token) - .await? - .unwrap(); + let read_password_reset_request = PasswordResetRequest::read_and_delete(pool, token).await?; assert_eq!( inserted_password_reset_request.id, read_password_reset_request.id @@ -102,8 +96,8 @@ mod tests { ); // Cannot reuse same token again - let read_password_reset_request = PasswordResetRequest::read_and_delete(pool, token).await?; - assert!(read_password_reset_request.is_none()); + let read_password_reset_request = PasswordResetRequest::read_and_delete(pool, token).await; + assert!(read_password_reset_request.is_err()); // Cleanup let num_deleted = Person::delete(pool, inserted_person.id).await?; diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index f2909218c..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,17 +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::{LemmyErrorType, LemmyResult}; #[async_trait] impl Crud for Person { @@ -29,14 +32,13 @@ impl Crud for Person { type IdType = PersonId; // Override this, so that you don't get back deleted - async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { + async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { let conn = &mut get_conn(pool).await?; person::table .filter(person::deleted.eq(false)) .find(person_id) .first(conn) .await - .optional() } async fn create(pool: &mut DbPool<'_>, form: &PersonInsertForm) -> Result { @@ -121,6 +123,20 @@ impl Person { .load::(conn) .await } + + pub async fn check_username_taken(pool: &mut DbPool<'_>, username: &str) -> LemmyResult<()> { + use diesel::dsl::{exists, select}; + let conn = &mut get_conn(pool).await?; + select(not(exists( + person::table + .filter(lower(person::name).eq(username.to_lowercase())) + .filter(person::local.eq(true)), + ))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::UsernameAlreadyExists.into()) + } } impl PersonInsertForm { @@ -183,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 } @@ -197,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 } } @@ -211,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 @@ -221,7 +244,6 @@ impl PersonFollower { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -230,16 +252,16 @@ 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, LemmyErrorType}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[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?; @@ -266,15 +288,12 @@ 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, }; - let read_person = Person::read(pool, inserted_person.id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + let read_person = Person::read(pool, inserted_person.id).await?; let update_person_form = PersonUpdateForm { actor_id: Some(inserted_person.actor_id.clone()), @@ -296,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?; @@ -319,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 0dbf003d8..363a2d3d1 100644 --- a/crates/db_schema/src/impls/person_block.rs +++ b/crates/db_schema/src/impls/person_block.rs @@ -1,27 +1,60 @@ use crate::{ newtypes::PersonId, - schema::person_block::dsl::{person_block, person_id, target_id}, - source::person_block::{PersonBlock, PersonBlockForm}, + 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}, + dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl PersonBlock { pub async fn read( pool: &mut DbPool<'_>, for_person_id: PersonId, for_recipient_id: PersonId, - ) -> Result { + ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(exists(person_block.find((for_person_id, for_recipient_id)))) - .get_result(conn) + select(not(exists(find_action( + person_actions::blocked, + (for_person_id, for_recipient_id), + )))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::PersonIsBlocked.into()) + } + + pub async fn for_person( + pool: &mut DbPool<'_>, + person_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let target_person_alias = diesel::alias!(person as person1); + + action_query(person_actions::blocked) + .inner_join(person::table.on(person_actions::person_id.eq(person::id))) + .inner_join( + 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_actions::person_id.eq(person_id)) + .filter(target_person_alias.field(person::deleted).eq(false)) + .order_by(person_actions::blocked) + .load::(conn) .await } } @@ -34,18 +67,29 @@ impl Blockable for PersonBlock { person_block_form: &PersonBlockForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_block) + 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_id, 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.find((person_block_form.person_id, person_block_form.target_id))) - .execute(conn) - .await + uplete::new( + person_actions::table.find((person_block_form.person_id, person_block_form.target_id)), + ) + .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 8e14bee9f..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::OptionalExtension, + diesel::{BoolExpressionMethods, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId, PostId}, - schema::{post, post_hide, post_like, post_read, post_saved}, + schema::{community, person, post, post_actions}, source::post::{ Post, PostHide, @@ -20,6 +20,8 @@ use crate::{ functions::coalesce, get_conn, naive_now, + now, + uplete, DbPool, DELETED_REPLACEMENT_TEXT, FETCH_LIMIT_MAX, @@ -30,10 +32,12 @@ use crate::{ use ::url::Url; use chrono::{DateTime, Utc}; use diesel::{ - dsl::insert_into, + dsl::{count, insert_into, not}, + expression::SelectableHelper, result::Error, DecoratableTarget, ExpressionMethods, + NullableExpressionMethods, QueryDsl, TextExpressionMethods, }; @@ -68,6 +72,10 @@ impl Crud for Post { } impl Post { + pub async fn read_xx(pool: &mut DbPool<'_>, id: PostId) -> Result { + let conn = &mut *get_conn(pool).await?; + post::table.find(id).first(conn).await + } pub async fn insert_apub( pool: &mut DbPool<'_>, timestamp: DateTime, @@ -140,7 +148,7 @@ impl Post { pool: &mut DbPool<'_>, for_creator_id: PersonId, for_community_id: Option, - new_removed: bool, + removed: bool, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; @@ -152,7 +160,7 @@ impl Post { } update - .set((post::removed.eq(new_removed), post::updated.eq(naive_now()))) + .set((post::removed.eq(removed), post::updated.eq(naive_now()))) .get_results::(conn) .await } @@ -169,6 +177,7 @@ impl Post { let object_id: DbUrl = object_id.into(); post::table .filter(post::ap_id.eq(object_id)) + .filter(post::scheduled_publish_time.is_null()) .first(conn) .await .optional() @@ -242,6 +251,28 @@ impl Post { .get_results::(conn) .await } + + pub async fn user_scheduled_post_count( + person_id: PersonId, + pool: &mut DbPool<'_>, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + post::table + .inner_join(person::table) + .inner_join(community::table) + // find all posts which have scheduled_publish_time that is in the future + .filter(post::scheduled_publish_time.is_not_null()) + .filter(coalesce(post::scheduled_publish_time, now()).gt(now())) + // make sure the post and community are still around + .filter(not(post::deleted.or(post::removed))) + .filter(not(community::removed.or(community::deleted))) + // only posts by specified user + .filter(post::creator_id.eq(person_id)) + .select(count(post::id)) + .first::(conn) + .await + } } #[async_trait] @@ -250,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 } @@ -262,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 } } @@ -275,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 } } @@ -301,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 } @@ -314,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 } } @@ -337,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 } @@ -350,22 +406,21 @@ 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 } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -385,8 +440,10 @@ 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; use pretty_assertions::assert_eq; use serial_test::serial; use std::collections::HashSet; @@ -394,41 +451,44 @@ mod tests { #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> LemmyResult<()> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "jim"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; - let new_community = CommunityInsertForm::builder() - .name("test community_3".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community_3".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let new_post2 = PostInsertForm::new( + "A test post 2".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post2 = Post::create(pool, &new_post2).await?; - let new_post2 = PostInsertForm::builder() - .name("A test post 2".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); - let inserted_post2 = Post::create(pool, &new_post2).await.unwrap(); + let new_scheduled_post = PostInsertForm { + scheduled_publish_time: Some(DateTime::from_timestamp_nanos(i64::MAX)), + ..PostInsertForm::new("beans".into(), inserted_person.id, inserted_community.id) + }; + let inserted_scheduled_post = Post::create(pool, &new_scheduled_post).await?; let expected_post = Post { id: inserted_post.id, @@ -448,14 +508,13 @@ mod tests { embed_description: None, embed_video_url: None, thumbnail_url: None, - ap_id: Url::parse(&format!("https://lemmy-alpha/post/{}", inserted_post.id)) - .unwrap() - .into(), + ap_id: Url::parse(&format!("https://lemmy-alpha/post/{}", inserted_post.id))?.into(), local: true, language_id: Default::default(), featured_community: false, featured_local: false, url_content_type: None, + scheduled_publish_time: None, }; // Post Like @@ -465,7 +524,7 @@ mod tests { score: 1, }; - let inserted_post_like = PostLike::like(pool, &post_like_form).await.unwrap(); + let inserted_post_like = PostLike::like(pool, &post_like_form).await?; let expected_post_like = PostLike { post_id: inserted_post.id, @@ -480,7 +539,7 @@ mod tests { person_id: inserted_person.id, }; - let inserted_post_saved = PostSaved::save(pool, &post_saved_form).await.unwrap(); + let inserted_post_saved = PostSaved::save(pool, &post_saved_form).await?; let expected_post_saved = PostSaved { post_id: inserted_post.id, @@ -494,48 +553,47 @@ mod tests { HashSet::from([inserted_post.id, inserted_post2.id]), inserted_person.id, ) - .await - .unwrap(); + .await?; assert_eq!(2, marked_as_read); - let read_post = Post::read(pool, inserted_post.id).await.unwrap().unwrap(); + let read_post = Post::read(pool, inserted_post.id).await?; let new_post_update = PostUpdateForm { name: Some("A test post".into()), ..Default::default() }; - let updated_post = Post::update(pool, inserted_post.id, &new_post_update) - .await - .unwrap(); + let updated_post = Post::update(pool, inserted_post.id, &new_post_update).await?; - let like_removed = PostLike::remove(pool, inserted_person.id, inserted_post.id) - .await - .unwrap(); - assert_eq!(1, like_removed); - let saved_removed = PostSaved::unsave(pool, &post_saved_form).await.unwrap(); - assert_eq!(1, saved_removed); + // Scheduled post count + let scheduled_post_count = Post::user_scheduled_post_count(inserted_person.id, pool).await?; + assert_eq!(1, scheduled_post_count); + + let like_removed = PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; + assert_eq!(uplete::Count::only_updated(1), like_removed); + let saved_removed = PostSaved::unsave(pool, &post_saved_form).await?; + 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 - .unwrap(); - assert_eq!(2, read_removed); + .await?; + assert_eq!(uplete::Count::only_deleted(2), read_removed); - let num_deleted = Post::delete(pool, inserted_post.id).await.unwrap() - + Post::delete(pool, inserted_post2.id).await.unwrap(); - assert_eq!(2, num_deleted); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let num_deleted = Post::delete(pool, inserted_post.id).await? + + Post::delete(pool, inserted_post2.id).await? + + Post::delete(pool, inserted_scheduled_post.id).await?; + assert_eq!(3, num_deleted); + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_post, read_post); assert_eq!(expected_post, inserted_post); assert_eq!(expected_post, updated_post); assert_eq!(expected_post_like, inserted_post_like); assert_eq!(expected_post_saved, inserted_post_saved); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index 7218ef468..e7d27aee9 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -80,8 +80,6 @@ impl Reportable for PostReport { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; @@ -95,29 +93,24 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use serial_test::serial; - async fn init(pool: &mut DbPool<'_>) -> (Person, PostReport) { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + async fn init(pool: &mut DbPool<'_>) -> Result<(Person, PostReport), Error> { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let person_form = PersonInsertForm::test_form(inserted_instance.id, "jim"); - let person = Person::create(pool, &person_form).await.unwrap(); + let person = Person::create(pool, &person_form).await?; - let community_form = CommunityInsertForm::builder() - .name("test community_4".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - let community = Community::create(pool, &community_form).await.unwrap(); + let community_form = CommunityInsertForm::new( + inserted_instance.id, + "test community_4".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; - let form = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(person.id) - .community_id(community.id) - .build(); - let post = Post::create(pool, &form).await.unwrap(); + let form = PostInsertForm::new("A test post".into(), person.id, community.id); + let post = Post::create(pool, &form).await?; let report_form = PostReportForm { post_id: post.id, @@ -125,46 +118,46 @@ mod tests { reason: "my reason".to_string(), ..Default::default() }; - let report = PostReport::report(pool, &report_form).await.unwrap(); - (person, report) + let report = PostReport::report(pool, &report_form).await?; + + Ok((person, report)) } #[tokio::test] #[serial] - async fn test_resolve_post_report() { - let pool = &build_db_pool_for_tests().await; + async fn test_resolve_post_report() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let (person, report) = init(pool).await; + let (person, report) = init(pool).await?; - let resolved_count = PostReport::resolve(pool, report.id, person.id) - .await - .unwrap(); + let resolved_count = PostReport::resolve(pool, report.id, person.id).await?; assert_eq!(resolved_count, 1); - let unresolved_count = PostReport::unresolve(pool, report.id, person.id) - .await - .unwrap(); + let unresolved_count = PostReport::unresolve(pool, report.id, person.id).await?; assert_eq!(unresolved_count, 1); - Person::delete(pool, person.id).await.unwrap(); - Post::delete(pool, report.post_id).await.unwrap(); + Person::delete(pool, person.id).await?; + Post::delete(pool, report.post_id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_resolve_all_post_reports() { - let pool = &build_db_pool_for_tests().await; + async fn test_resolve_all_post_reports() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let (person, report) = init(pool).await; + let (person, report) = init(pool).await?; - let resolved_count = PostReport::resolve_all_for_object(pool, report.post_id, person.id) - .await - .unwrap(); + let resolved_count = + PostReport::resolve_all_for_object(pool, report.post_id, person.id).await?; assert_eq!(resolved_count, 1); - Person::delete(pool, person.id).await.unwrap(); - Post::delete(pool, report.post_id).await.unwrap(); + Person::delete(pool, person.id).await?; + Post::delete(pool, report.post_id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index fe3629a1a..e08b4cf7f 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -85,8 +85,6 @@ impl PrivateMessage { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -98,37 +96,34 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; use url::Url; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> LemmyResult<()> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let creator_form = PersonInsertForm::test_form(inserted_instance.id, "creator_pm"); - let inserted_creator = Person::create(pool, &creator_form).await.unwrap(); + let inserted_creator = Person::create(pool, &creator_form).await?; let recipient_form = PersonInsertForm::test_form(inserted_instance.id, "recipient_pm"); - let inserted_recipient = Person::create(pool, &recipient_form).await.unwrap(); + let inserted_recipient = Person::create(pool, &recipient_form).await?; - let private_message_form = PrivateMessageInsertForm::builder() - .content("A test private message".into()) - .creator_id(inserted_creator.id) - .recipient_id(inserted_recipient.id) - .build(); + let private_message_form = PrivateMessageInsertForm::new( + inserted_creator.id, + inserted_recipient.id, + "A test private message".into(), + ); - let inserted_private_message = PrivateMessage::create(pool, &private_message_form) - .await - .unwrap(); + let inserted_private_message = PrivateMessage::create(pool, &private_message_form).await?; let expected_private_message = PrivateMessage { id: inserted_private_message.id, @@ -142,16 +137,12 @@ mod tests { ap_id: Url::parse(&format!( "https://lemmy-alpha/private_message/{}", inserted_private_message.id - )) - .unwrap() + ))? .into(), local: true, }; - let read_private_message = PrivateMessage::read(pool, inserted_private_message.id) - .await - .unwrap() - .unwrap(); + let read_private_message = PrivateMessage::read(pool, inserted_private_message.id).await?; let private_message_update_form = PrivateMessageUpdateForm { content: Some("A test private message".into()), @@ -162,8 +153,7 @@ mod tests { inserted_private_message.id, &private_message_update_form, ) - .await - .unwrap(); + .await?; let deleted_private_message = PrivateMessage::update( pool, @@ -173,8 +163,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; let marked_read_private_message = PrivateMessage::update( pool, inserted_private_message.id, @@ -183,16 +172,17 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); - Person::delete(pool, inserted_creator.id).await.unwrap(); - Person::delete(pool, inserted_recipient.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + .await?; + Person::delete(pool, inserted_creator.id).await?; + Person::delete(pool, inserted_recipient.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_private_message, read_private_message); assert_eq!(expected_private_message, updated_private_message); assert_eq!(expected_private_message, inserted_private_message); assert!(deleted_private_message.deleted); assert!(marked_read_private_message.read); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/registration_application.rs b/crates/db_schema/src/impls/registration_application.rs index 055ffb51f..d9777919d 100644 --- a/crates/db_schema/src/impls/registration_application.rs +++ b/crates/db_schema/src/impls/registration_application.rs @@ -1,5 +1,4 @@ use crate::{ - diesel::OptionalExtension, newtypes::{LocalUserId, RegistrationApplicationId}, schema::registration_application::dsl::{local_user_id, registration_application}, source::registration_application::{ @@ -44,12 +43,11 @@ impl RegistrationApplication { pub async fn find_by_local_user_id( pool: &mut DbPool<'_>, local_user_id_: LocalUserId, - ) -> Result, Error> { + ) -> Result { let conn = &mut get_conn(pool).await?; registration_application .filter(local_user_id.eq(local_user_id_)) .first(conn) .await - .optional() } } diff --git a/crates/db_schema/src/impls/secret.rs b/crates/db_schema/src/impls/secret.rs index 1365ea838..bfff270b6 100644 --- a/crates/db_schema/src/impls/secret.rs +++ b/crates/db_schema/src/impls/secret.rs @@ -1,5 +1,4 @@ use crate::{ - diesel::OptionalExtension, schema::secret::dsl::secret, source::secret::Secret, utils::{get_conn, DbPool}, @@ -10,12 +9,12 @@ use diesel_async::RunQueryDsl; impl Secret { /// Initialize the Secrets from the DB. /// Warning: You should only call this once. - pub async fn init(pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn init(pool: &mut DbPool<'_>) -> Result { Self::read_secrets(pool).await } - async fn read_secrets(pool: &mut DbPool<'_>) -> Result, Error> { + async fn read_secrets(pool: &mut DbPool<'_>) -> Result { let conn = &mut get_conn(pool).await?; - secret.first(conn).await.optional() + secret.first(conn).await } } diff --git a/crates/db_schema/src/impls/site.rs b/crates/db_schema/src/impls/site.rs index 9dbd2401d..e993639fa 100644 --- a/crates/db_schema/src/impls/site.rs +++ b/crates/db_schema/src/impls/site.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{DbUrl, InstanceId, SiteId}, - schema::site, + schema::{local_site, site}, source::{ actor_language::SiteLanguage, site::{Site, SiteInsertForm, SiteUpdateForm}, @@ -10,6 +10,7 @@ use crate::{ }; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl}; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use url::Url; #[async_trait] @@ -19,7 +20,7 @@ impl Crud for Site { type IdType = SiteId; /// Use SiteView::read_local, or Site::read_from_apub_id instead - async fn read(_pool: &mut DbPool<'_>, _site_id: SiteId) -> Result, Error> { + async fn read(_pool: &mut DbPool<'_>, _site_id: SiteId) -> Result { Err(Error::NotFound) } @@ -102,4 +103,18 @@ impl Site { url.set_query(None); url } + + pub async fn read_local(pool: &mut DbPool<'_>) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + + Ok( + site::table + .inner_join(local_site::table) + .select(site::all_columns) + .first(conn) + .await + .optional()? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?, + ) + } } diff --git a/crates/db_schema/src/impls/tagline.rs b/crates/db_schema/src/impls/tagline.rs index be4860e17..aa5841020 100644 --- a/crates/db_schema/src/impls/tagline.rs +++ b/crates/db_schema/src/impls/tagline.rs @@ -1,58 +1,59 @@ use crate::{ - newtypes::LocalSiteId, - schema::tagline::dsl::{local_site_id, tagline}, - source::tagline::{Tagline, TaglineForm}, - utils::{get_conn, DbPool}, + newtypes::TaglineId, + schema::tagline::dsl::{published, tagline}, + source::tagline::{Tagline, TaglineInsertForm, TaglineUpdateForm}, + traits::Crud, + utils::{get_conn, limit_and_offset, DbPool}, }; use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel_async::RunQueryDsl; -impl Tagline { - pub async fn replace( - pool: &mut DbPool<'_>, - for_local_site_id: LocalSiteId, - list_content: Option>, - ) -> Result, Error> { +#[async_trait] +impl Crud for Tagline { + type InsertForm = TaglineInsertForm; + type UpdateForm = TaglineUpdateForm; + type IdType = TaglineId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - if let Some(list) = list_content { - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - Self::clear(conn).await?; - - for item in list { - let form = TaglineForm { - local_site_id: for_local_site_id, - content: item, - updated: None, - }; - insert_into(tagline) - .values(form) - .get_result::(conn) - .await?; - } - Self::get_all(&mut conn.into(), for_local_site_id).await - }) as _ - }) - .await - } else { - Self::get_all(&mut conn.into(), for_local_site_id).await - } + insert_into(tagline) + .values(form) + .get_result::(conn) + .await } - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(tagline).execute(conn).await - } - - pub async fn get_all( + async fn update( pool: &mut DbPool<'_>, - for_local_site_id: LocalSiteId, - ) -> Result, Error> { + tagline_id: TaglineId, + new_tagline: &Self::UpdateForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - tagline - .filter(local_site_id.eq(for_local_site_id)) - .get_results::(conn) + diesel::update(tagline.find(tagline_id)) + .set(new_tagline) + .get_result::(conn) .await } } + +impl Tagline { + pub async fn list( + pool: &mut DbPool<'_>, + page: Option, + limit: Option, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let (limit, offset) = limit_and_offset(page, limit)?; + tagline + .order(published.desc()) + .offset(offset) + .limit(limit) + .get_results::(conn) + .await + } + + pub async fn get_random(pool: &mut DbPool<'_>) -> Result { + let conn = &mut get_conn(pool).await?; + sql_function!(fn random() -> Text); + tagline.order(random()).limit(1).first::(conn).await + } +} diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 9d46d5d96..6e1abaf0f 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -22,15 +22,14 @@ pub mod newtypes; pub mod sensitive; #[cfg(feature = "full")] #[rustfmt::skip] -#[allow(clippy::wildcard_imports)] 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; @@ -53,13 +52,13 @@ use ts_rs::TS; #[cfg_attr(feature = "full", derive(DbEnum, TS))] #[cfg_attr( feature = "full", - ExistingTypePath = "crate::schema::sql_types::SortTypeEnum" + ExistingTypePath = "crate::schema::sql_types::PostSortTypeEnum" )] #[cfg_attr(feature = "full", DbValueStyle = "verbatim")] #[cfg_attr(feature = "full", ts(export))] // TODO add the controversial and scaled rankings to the doc below /// The post sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html -pub enum SortType { +pub enum PostSortType { #[default] Active, Hot, @@ -82,11 +81,19 @@ pub enum SortType { Scaled, } -#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] +#[derive( + EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash, +)] +#[cfg_attr(feature = "full", derive(DbEnum, TS))] +#[cfg_attr( + feature = "full", + ExistingTypePath = "crate::schema::sql_types::CommentSortTypeEnum" +)] +#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] #[cfg_attr(feature = "full", ts(export))] /// The comment sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html pub enum CommentSortType { + #[default] Hot, Top, New, @@ -169,7 +176,6 @@ pub enum SearchType { Posts, Communities, Users, - Url, } #[derive(EnumString, Display, Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash)] @@ -180,6 +186,7 @@ pub enum SubscribedType { Subscribed, NotSubscribed, Pending, + ApprovalRequired, } #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] @@ -230,14 +237,35 @@ 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( + EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash, +)] +#[cfg_attr(feature = "full", derive(DbEnum, TS))] +#[cfg_attr( + feature = "full", + ExistingTypePath = "crate::schema::sql_types::FederationModeEnum" +)] +#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] +#[cfg_attr(feature = "full", ts(export))] +/// The federation mode for an item +pub enum FederationMode { + #[default] + /// Allows all + All, + /// Allows only local + Local, + /// Disables + Disable, } /// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c715305bb..c28be8222 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -148,12 +148,24 @@ pub struct LocalSiteId(i32); /// The custom emoji id. pub struct CustomEmojiId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The tagline id. +pub struct TaglineId(i32); + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The registration application id. pub struct RegistrationApplicationId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The oauth provider id. +pub struct OAuthProviderId(pub i32); + #[cfg(feature = "full")] #[derive(Serialize, Deserialize)] #[serde(remote = "Ltree")] @@ -162,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 { @@ -179,13 +192,13 @@ impl Display for DbUrl { } // the project doesn't compile with From -#[allow(clippy::from_over_into)] +#[expect(clippy::from_over_into)] impl Into for Url { fn into(self) -> DbUrl { DbUrl(Box::new(self)) } } -#[allow(clippy::from_over_into)] +#[expect(clippy::from_over_into)] impl Into for DbUrl { fn into(self) -> Url { *self.0 @@ -236,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 8f5a63559..7e4302eb9 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -1,33 +1,45 @@ // @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::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::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::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "registration_mode_enum"))] pub struct RegistrationModeEnum; - - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "sort_type_enum"))] - pub struct SortTypeEnum; } diesel::table! { @@ -98,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, @@ -111,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, @@ -145,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; @@ -163,7 +167,7 @@ diesel::table! { name -> Varchar, #[max_length = 255] title -> Varchar, - description -> Nullable, + sidebar -> Nullable, removed -> Bool, published -> Timestamptz, updated -> Nullable, @@ -181,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, @@ -191,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, } } @@ -210,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, @@ -234,27 +238,9 @@ 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, - local_site_id -> Int4, #[max_length = 128] shortcode -> Varchar, image_url -> Text, @@ -333,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, } } @@ -363,14 +349,14 @@ diesel::table! { use super::sql_types::ListingTypeEnum; use super::sql_types::RegistrationModeEnum; use super::sql_types::PostListingModeEnum; - use super::sql_types::SortTypeEnum; + use super::sql_types::PostSortTypeEnum; + use super::sql_types::CommentSortTypeEnum; + use super::sql_types::FederationModeEnum; local_site (id) { id -> Int4, site_id -> Int4, site_setup -> Bool, - enable_downvotes -> Bool, - enable_nsfw -> Bool, community_creation_admin_only -> Bool, require_email_verification -> Bool, application_question -> Nullable, @@ -392,7 +378,13 @@ diesel::table! { reports_email_admins -> Bool, federation_signed_fetch -> Bool, default_post_listing_mode -> PostListingModeEnum, - default_sort_type -> SortTypeEnum, + default_post_sort_type -> PostSortTypeEnum, + default_comment_sort_type -> CommentSortTypeEnum, + oauth_registration -> Bool, + post_upvotes -> FederationModeEnum, + post_downvotes -> FederationModeEnum, + comment_upvotes -> FederationModeEnum, + comment_downvotes -> FederationModeEnum, } } @@ -429,24 +421,24 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; - use super::sql_types::SortTypeEnum; + use super::sql_types::PostSortTypeEnum; use super::sql_types::ListingTypeEnum; use super::sql_types::PostListingModeEnum; + use super::sql_types::CommentSortTypeEnum; local_user (id) { id -> Int4, person_id -> Int4, - password_encrypted -> Text, + password_encrypted -> Nullable, email -> Nullable, show_nsfw -> Bool, theme -> Text, - default_sort_type -> SortTypeEnum, + default_post_sort_type -> PostSortTypeEnum, default_listing_type -> ListingTypeEnum, #[max_length = 20] interface_language -> Varchar, show_avatars -> Bool, send_notifications_to_email -> Bool, - show_scores -> Bool, show_bot_accounts -> Bool, show_read_posts -> Bool, email_verified -> Bool, @@ -454,14 +446,15 @@ diesel::table! { totp_2fa_secret -> Nullable, open_links_in_new_tab -> Bool, blur_nsfw -> Bool, - auto_expand -> Bool, infinite_scroll_enabled -> Bool, admin -> Bool, post_listing_mode -> PostListingModeEnum, 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, } } @@ -613,6 +606,36 @@ diesel::table! { } } +diesel::table! { + oauth_account (oauth_provider_id, local_user_id) { + local_user_id -> Int4, + oauth_provider_id -> Int4, + oauth_user_id -> Text, + published -> Timestamptz, + updated -> Nullable, + } +} + +diesel::table! { + oauth_provider (id) { + id -> Int4, + display_name -> Text, + issuer -> Text, + authorization_endpoint -> Text, + token_endpoint -> Text, + userinfo_endpoint -> Text, + id_claim -> Text, + client_id -> Text, + client_secret -> Text, + scopes -> Text, + auto_verify_email -> Bool, + account_linking_enabled -> Bool, + enabled -> Bool, + published -> Timestamptz, + updated -> Nullable, + } +} + diesel::table! { password_reset_request (id) { id -> Int4, @@ -644,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, @@ -653,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, @@ -670,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, @@ -697,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, @@ -734,6 +739,21 @@ diesel::table! { featured_local -> Bool, url_content_type -> Nullable, alt_text -> Nullable, + scheduled_publish_time -> Nullable, + } +} + +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, } } @@ -759,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, @@ -801,14 +796,6 @@ diesel::table! { } } -diesel::table! { - post_saved (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { previously_run_sql (id) { id -> Bool, @@ -944,7 +931,6 @@ diesel::table! { diesel::table! { tagline (id) { id -> Int4, - local_site_id -> Int4, content -> Text, published -> Timestamptz, updated -> Nullable, @@ -960,35 +946,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 -> local_site (local_site_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)); @@ -1012,30 +987,24 @@ diesel::joinable!(mod_remove_community -> person (mod_person_id)); diesel::joinable!(mod_remove_post -> person (mod_person_id)); diesel::joinable!(mod_remove_post -> post (post_id)); diesel::joinable!(mod_transfer_community -> community (community_id)); +diesel::joinable!(oauth_account -> local_user (local_user_id)); +diesel::joinable!(oauth_account -> oauth_provider (oauth_provider_id)); diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(person -> instance (instance_id)); 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)); @@ -1043,7 +1012,6 @@ diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); diesel::joinable!(site_language -> site (site_id)); -diesel::joinable!(tagline -> local_site (local_site_id)); diesel::allow_tables_to_appear_in_same_query!( admin_purge_comment, @@ -1052,18 +1020,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, @@ -1072,7 +1036,7 @@ diesel::allow_tables_to_appear_in_same_query!( federation_queue_state, image_details, instance, - instance_block, + instance_actions, language, local_image, local_site, @@ -1093,21 +1057,18 @@ diesel::allow_tables_to_appear_in_same_query!( mod_remove_community, mod_remove_post, mod_transfer_community, + oauth_account, + 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, previously_run_sql, private_message, private_message_report, diff --git a/crates/db_schema/src/schema_setup.rs b/crates/db_schema/src/schema_setup.rs index 7ce04abb6..054940cf2 100644 --- a/crates/db_schema/src/schema_setup.rs +++ b/crates/db_schema/src/schema_setup.rs @@ -20,7 +20,6 @@ use diesel::{ use diesel_migrations::MigrationHarness; use lemmy_utils::{error::LemmyResult, settings::SETTINGS}; use std::time::Instant; -use tracing::info; diesel::table! { pg_namespace (nspname) { @@ -74,7 +73,7 @@ impl<'a> MigrationHarnessWrapper<'a> { let duration = start_time.elapsed().as_millis(); let name = migration.name(); - info!("{duration}ms run {name}"); + println!("{duration}ms run {name}"); result } @@ -117,7 +116,7 @@ impl<'a> MigrationHarness for MigrationHarnessWrapper<'a> { let duration = start_time.elapsed().as_millis(); let name = migration.name(); - info!("{duration}ms revert {name}"); + println!("{duration}ms revert {name}"); result } @@ -197,9 +196,9 @@ pub fn run(options: Options) -> LemmyResult { // Block concurrent attempts to run migrations until `conn` is closed, and disable the // trigger that prevents the Diesel CLI from running migrations - info!("Waiting for lock..."); + println!("Waiting for lock..."); conn.batch_execute("SELECT pg_advisory_lock(0);")?; - info!("Running Database migrations (This may take a long time)..."); + println!("Running Database migrations (This may take a long time)..."); // Drop `r` schema, so migrations don't need to be made to work both with and without things in // it existing @@ -232,7 +231,7 @@ pub fn run(options: Options) -> LemmyResult { Branch::ReplaceableSchemaNotRebuilt }; - info!("Database migrations complete."); + println!("Database migrations complete."); Ok(output) } 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 74ae0b7f6..be9aa7873 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -2,15 +2,16 @@ 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; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -31,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, @@ -51,24 +53,28 @@ pub struct Comment { pub language_id: LanguageId, } -#[derive(Debug, Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Debug, Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = comment))] pub struct CommentInsertForm { - #[builder(!default)] pub creator_id: PersonId, - #[builder(!default)] pub post_id: PostId, - #[builder(!default)] pub content: String, + #[new(default)] pub removed: Option, + #[new(default)] pub published: Option>, + #[new(default)] pub updated: Option>, + #[new(default)] pub deleted: Option, + #[new(default)] pub ap_id: Option, + #[new(default)] pub local: Option, + #[new(default)] pub distinguished: Option, + #[new(default)] pub language_id: Option, } @@ -93,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, } @@ -120,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 fe7f120ec..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,11 +7,13 @@ 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; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -25,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, @@ -46,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)] @@ -55,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. @@ -69,40 +73,59 @@ 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, TypedBuilder, Default)] -#[builder(field_defaults(default))] +#[derive(Debug, Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = community))] pub struct CommunityInsertForm { - #[builder(!default)] - pub name: String, - #[builder(!default)] - pub title: String, - pub description: Option, - pub removed: Option, - pub published: Option>, - pub updated: Option>, - pub deleted: Option, - pub nsfw: Option, - pub actor_id: Option, - pub local: Option, - pub private_key: Option, - pub public_key: String, - pub last_refreshed_at: Option>, - pub icon: Option, - 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, - #[builder(!default)] pub instance_id: InstanceId, + pub name: String, + pub title: String, + pub public_key: String, + #[new(default)] + pub sidebar: Option, + #[new(default)] + pub removed: Option, + #[new(default)] + pub published: Option>, + #[new(default)] + pub updated: Option>, + #[new(default)] + pub deleted: Option, + #[new(default)] + pub nsfw: Option, + #[new(default)] + pub actor_id: Option, + #[new(default)] + pub local: Option, + #[new(default)] + pub private_key: Option, + #[new(default)] + pub last_refreshed_at: Option>, + #[new(default)] + pub icon: Option, + #[new(default)] + pub banner: Option, + #[new(default)] + pub followers_url: Option, + #[new(default)] + pub inbox_url: Option, + #[new(default)] + pub moderators_url: Option, + #[new(default)] + pub featured_url: Option, + #[new(default)] + pub hidden: Option, + #[new(default)] + pub posting_restricted_to_mods: Option, + #[new(default)] pub visibility: Option, + #[new(default)] + pub description: Option, } #[derive(Debug, Clone, Default)] @@ -110,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>>, @@ -125,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)] @@ -142,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, @@ -168,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", @@ -196,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 3217c9736..bb95cb7c8 100644 --- a/crates/db_schema/src/source/custom_emoji.rs +++ b/crates/db_schema/src/source/custom_emoji.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{CustomEmojiId, DbUrl, LocalSiteId}; +use crate::newtypes::{CustomEmojiId, DbUrl}; #[cfg(feature = "full")] use crate::schema::custom_emoji; use chrono::{DateTime, Utc}; @@ -6,49 +6,39 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable, TS) -)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] -#[cfg_attr( - feature = "full", - diesel(belongs_to(crate::source::local_site::LocalSite)) -)] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A custom emoji. pub struct CustomEmoji { pub id: CustomEmojiId, - pub local_site_id: LocalSiteId, pub shortcode: String, pub image_url: DbUrl, pub alt_text: String, pub category: String, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] pub struct CustomEmojiInsertForm { - pub local_site_id: LocalSiteId, pub shortcode: String, pub image_url: DbUrl, pub alt_text: String, pub category: String, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] pub struct CustomEmojiUpdateForm { - pub local_site_id: LocalSiteId, pub image_url: DbUrl, pub alt_text: String, pub category: String, diff --git a/crates/db_schema/src/source/custom_emoji_keyword.rs b/crates/db_schema/src/source/custom_emoji_keyword.rs index 34ee071b5..a47ba411e 100644 --- a/crates/db_schema/src/source/custom_emoji_keyword.rs +++ b/crates/db_schema/src/source/custom_emoji_keyword.rs @@ -4,7 +4,6 @@ use crate::schema::custom_emoji_keyword; use serde::{Deserialize, Serialize}; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] #[cfg_attr( @@ -25,7 +24,7 @@ pub struct CustomEmojiKeyword { pub keyword: String, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = custom_emoji_keyword))] pub struct CustomEmojiKeywordInsertForm { 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/image_upload.rs b/crates/db_schema/src/source/image_upload.rs index b72c55065..db840dc1d 100644 --- a/crates/db_schema/src/source/image_upload.rs +++ b/crates/db_schema/src/source/image_upload.rs @@ -7,7 +7,6 @@ use serde_with::skip_serializing_none; use std::fmt::Debug; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] @@ -30,7 +29,7 @@ pub struct ImageUpload { pub published: DateTime, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = image_upload))] pub struct ImageUploadForm { diff --git a/crates/db_schema/src/source/images.rs b/crates/db_schema/src/source/images.rs index 0dea4b84f..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, @@ -51,13 +52,6 @@ pub struct RemoteImage { pub published: DateTime, } -#[derive(Debug, Clone)] -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = remote_image))] -pub struct RemoteImageForm { - pub link: DbUrl, -} - #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] diff --git a/crates/db_schema/src/source/instance.rs b/crates/db_schema/src/source/instance.rs index 98e0d401b..f622751cc 100644 --- a/crates/db_schema/src/source/instance.rs +++ b/crates/db_schema/src/source/instance.rs @@ -7,7 +7,6 @@ use serde_with::skip_serializing_none; use std::fmt::Debug; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -20,19 +19,23 @@ 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, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = instance))] pub struct InstanceForm { - #[builder(!default)] pub domain: String, + #[new(default)] pub software: Option, + #[new(default)] pub version: Option, + #[new(default)] pub updated: 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 05583c065..b5bcebc58 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -2,17 +2,18 @@ use crate::schema::local_site; use crate::{ newtypes::{LocalSiteId, SiteId}, + CommentSortType, + FederationMode, ListingType, PostListingMode, + PostSortType, RegistrationMode, - SortType, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)] @@ -27,15 +28,12 @@ pub struct LocalSite { pub site_id: SiteId, /// True if the site is set up. pub site_setup: bool, - /// Whether downvotes are enabled. - pub enable_downvotes: bool, - /// Whether NSFW is enabled. - pub enable_nsfw: bool, /// Whether only admins can create communities. pub community_creation_admin_only: bool, /// 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, @@ -43,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, @@ -59,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. @@ -68,39 +69,79 @@ pub struct LocalSite { pub federation_signed_fetch: bool, /// Default value for [LocalSite.post_listing_mode] pub default_post_listing_mode: PostListingMode, - /// Default value for [LocalUser.post_listing_mode] - pub default_sort_type: SortType, + /// Default value for [LocalUser.post_sort_type] + pub default_post_sort_type: PostSortType, + /// Default value for [LocalUser.comment_sort_type] + pub default_comment_sort_type: CommentSortType, + /// Whether or not external auth methods can auto-register users. + pub oauth_registration: bool, + /// What kind of post upvotes your site allows. + pub post_upvotes: FederationMode, + /// What kind of post downvotes your site allows. + pub post_downvotes: FederationMode, + /// What kind of comment upvotes your site allows. + pub comment_upvotes: FederationMode, + /// What kind of comment downvotes your site allows. + pub comment_downvotes: FederationMode, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable))] #[cfg_attr(feature = "full", diesel(table_name = local_site))] pub struct LocalSiteInsertForm { - #[builder(!default)] pub site_id: SiteId, + #[new(default)] pub site_setup: Option, - pub enable_downvotes: Option, - pub enable_nsfw: Option, + #[new(default)] pub community_creation_admin_only: Option, + #[new(default)] pub require_email_verification: Option, + #[new(default)] pub application_question: Option, + #[new(default)] pub private_instance: Option, + #[new(default)] pub default_theme: Option, + #[new(default)] pub default_post_listing_type: Option, + #[new(default)] pub legal_information: Option, + #[new(default)] pub hide_modlog_mod_names: Option, + #[new(default)] pub application_email_admins: Option, + #[new(default)] pub slur_filter_regex: Option, + #[new(default)] pub actor_name_max_length: Option, + #[new(default)] pub federation_enabled: Option, + #[new(default)] pub captcha_enabled: Option, + #[new(default)] pub captcha_difficulty: Option, + #[new(default)] pub registration_mode: Option, + #[new(default)] pub reports_email_admins: Option, + #[new(default)] pub federation_signed_fetch: Option, + #[new(default)] pub default_post_listing_mode: Option, - pub default_sort_type: Option, + #[new(default)] + pub default_post_sort_type: Option, + #[new(default)] + pub default_comment_sort_type: Option, + #[new(default)] + pub oauth_registration: Option, + #[new(default)] + pub post_upvotes: Option, + #[new(default)] + pub post_downvotes: Option, + #[new(default)] + pub comment_upvotes: Option, + #[new(default)] + pub comment_downvotes: Option, } #[derive(Clone, Default)] @@ -108,8 +149,6 @@ pub struct LocalSiteInsertForm { #[cfg_attr(feature = "full", diesel(table_name = local_site))] pub struct LocalSiteUpdateForm { pub site_setup: Option, - pub enable_downvotes: Option, - pub enable_nsfw: Option, pub community_creation_admin_only: Option, pub require_email_verification: Option, pub application_question: Option>, @@ -129,5 +168,11 @@ pub struct LocalSiteUpdateForm { pub updated: Option>>, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, - pub default_sort_type: Option, + pub default_post_sort_type: Option, + pub default_comment_sort_type: Option, + pub oauth_registration: Option, + pub post_upvotes: Option, + pub post_downvotes: Option, + pub comment_upvotes: Option, + pub comment_downvotes: Option, } 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 6ba3df59e..af424a248 100644 --- a/crates/db_schema/src/source/local_site_rate_limit.rs +++ b/crates/db_schema/src/source/local_site_rate_limit.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] @@ -35,31 +34,44 @@ 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, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable))] #[cfg_attr(feature = "full", diesel(table_name = local_site_rate_limit))] pub struct LocalSiteRateLimitInsertForm { - #[builder(!default)] pub local_site_id: LocalSiteId, + #[new(default)] pub message: Option, + #[new(default)] pub message_per_second: Option, + #[new(default)] pub post: Option, + #[new(default)] pub post_per_second: Option, + #[new(default)] pub register: Option, + #[new(default)] pub register_per_second: Option, + #[new(default)] pub image: Option, + #[new(default)] pub image_per_second: Option, + #[new(default)] pub comment: Option, + #[new(default)] pub comment_per_second: Option, + #[new(default)] pub search: Option, + #[new(default)] pub search_per_second: Option, + #[new(default)] pub import_user_settings: Option, + #[new(default)] pub import_user_settings_per_second: Option, } 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 c7a5b5224..fd15253cc 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -3,9 +3,10 @@ use crate::schema::local_user; use crate::{ newtypes::{LocalUserId, PersonId}, sensitive::SensitiveString, + CommentSortType, ListingType, PostListingMode, - SortType, + PostSortType, }; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -13,31 +14,30 @@ use serde_with::skip_serializing_none; use ts_rs::TS; #[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = local_user))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] +#[serde(default)] /// A local user. pub struct LocalUser { pub id: LocalUserId, /// The person_id for the local user. pub person_id: PersonId, #[serde(skip)] - pub password_encrypted: SensitiveString, + pub password_encrypted: Option, + #[cfg_attr(feature = "full", ts(optional))] pub email: Option, /// Whether to show NSFW content. pub show_nsfw: bool, pub theme: String, - pub default_sort_type: SortType, + pub default_post_sort_type: PostSortType, pub default_listing_type: ListingType, pub interface_language: String, /// Whether to show avatars. pub show_avatars: bool, pub send_notifications_to_email: bool, - /// Whether to show comment / post scores. - // TODO now that there is a vote_display_mode, this can be gotten rid of in future releases. - pub show_scores: bool, /// Whether to show bot accounts. pub show_bot_accounts: bool, /// Whether to show read posts. @@ -51,7 +51,6 @@ pub struct LocalUser { /// Open links in a new tab. pub open_links_in_new_tab: bool, pub blur_nsfw: bool, - pub auto_expand: bool, /// Whether infinite scroll is enabled. pub infinite_scroll_enabled: bool, /// Whether the person is an admin. @@ -64,8 +63,11 @@ 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, } #[derive(Clone, derive_new::new)] @@ -73,7 +75,7 @@ pub struct LocalUser { #[cfg_attr(feature = "full", diesel(table_name = local_user))] pub struct LocalUserInsertForm { pub person_id: PersonId, - pub password_encrypted: String, + pub password_encrypted: Option, #[new(default)] pub email: Option, #[new(default)] @@ -81,7 +83,7 @@ pub struct LocalUserInsertForm { #[new(default)] pub theme: Option, #[new(default)] - pub default_sort_type: Option, + pub default_post_sort_type: Option, #[new(default)] pub default_listing_type: Option, #[new(default)] @@ -93,8 +95,6 @@ pub struct LocalUserInsertForm { #[new(default)] pub show_bot_accounts: Option, #[new(default)] - pub show_scores: Option, - #[new(default)] pub show_read_posts: Option, #[new(default)] pub email_verified: Option, @@ -107,8 +107,6 @@ pub struct LocalUserInsertForm { #[new(default)] pub blur_nsfw: Option, #[new(default)] - pub auto_expand: Option, - #[new(default)] pub infinite_scroll_enabled: Option, #[new(default)] pub admin: Option, @@ -121,7 +119,11 @@ 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, } #[derive(Clone, Default)] @@ -132,25 +134,25 @@ pub struct LocalUserUpdateForm { pub email: Option>, pub show_nsfw: Option, pub theme: Option, - pub default_sort_type: Option, + pub default_post_sort_type: Option, pub default_listing_type: Option, pub interface_language: Option, pub show_avatars: Option, pub send_notifications_to_email: Option, pub show_bot_accounts: Option, - pub show_scores: Option, pub show_read_posts: Option, pub email_verified: Option, pub accepted_application: Option, pub totp_2fa_secret: Option>, pub open_links_in_new_tab: Option, pub blur_nsfw: Option, - pub auto_expand: Option, pub infinite_scroll_enabled: Option, pub admin: Option, pub post_listing_mode: Option, 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/local_user_vote_display_mode.rs b/crates/db_schema/src/source/local_user_vote_display_mode.rs index 314d99e4a..06a433034 100644 --- a/crates/db_schema/src/source/local_user_vote_display_mode.rs +++ b/crates/db_schema/src/source/local_user_vote_display_mode.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)] @@ -20,6 +19,7 @@ use typed_builder::TypedBuilder; #[cfg_attr(feature = "full", ts(export))] /// The vote display settings for your user. pub struct LocalUserVoteDisplayMode { + #[serde(skip)] pub local_user_id: LocalUserId, pub score: bool, pub upvotes: bool, @@ -27,16 +27,18 @@ pub struct LocalUserVoteDisplayMode { pub upvote_percentage: bool, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable))] #[cfg_attr(feature = "full", diesel(table_name = local_user_vote_display_mode))] pub struct LocalUserVoteDisplayModeInsertForm { - #[builder(!default)] pub local_user_id: LocalUserId, + #[new(default)] pub score: Option, + #[new(default)] pub upvotes: Option, + #[new(default)] pub downvotes: Option, + #[new(default)] pub upvote_percentage: 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/mod.rs b/crates/db_schema/src/source/mod.rs index bbc8aafa2..377c1aaef 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -27,6 +27,8 @@ pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; pub mod moderator; +pub mod oauth_account; +pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; 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 new file mode 100644 index 000000000..b7d190c35 --- /dev/null +++ b/crates/db_schema/src/source/oauth_account.rs @@ -0,0 +1,33 @@ +use crate::newtypes::{LocalUserId, OAuthProviderId}; +#[cfg(feature = "full")] +use crate::schema::oauth_account; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_account))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// An auth account method. +pub struct OAuthAccount { + pub local_user_id: LocalUserId, + pub oauth_provider_id: OAuthProviderId, + pub oauth_user_id: String, + pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] + pub updated: Option>, +} + +#[derive(Debug, Clone, derive_new::new)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_account))] +pub struct OAuthAccountInsertForm { + pub local_user_id: LocalUserId, + pub oauth_provider_id: OAuthProviderId, + pub oauth_user_id: String, +} diff --git a/crates/db_schema/src/source/oauth_provider.rs b/crates/db_schema/src/source/oauth_provider.rs new file mode 100644 index 000000000..a70405a5e --- /dev/null +++ b/crates/db_schema/src/source/oauth_provider.rs @@ -0,0 +1,123 @@ +#[cfg(feature = "full")] +use crate::schema::oauth_provider; +use crate::{ + newtypes::{DbUrl, OAuthProviderId}, + sensitive::SensitiveString, +}; +use chrono::{DateTime, Utc}; +use serde::{ + ser::{SerializeStruct, Serializer}, + Deserialize, + Serialize, +}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// oauth provider with client_secret - should never be sent to the client +pub struct OAuthProvider { + pub id: OAuthProviderId, + /// The OAuth 2.0 provider name displayed to the user on the Login page + pub display_name: String, + /// The issuer url of the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub issuer: DbUrl, + /// The authorization endpoint is used to interact with the resource owner and obtain an + /// authorization grant. This is usually provided by the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub authorization_endpoint: DbUrl, + /// The token endpoint is used by the client to obtain an access token by presenting its + /// authorization grant or refresh token. This is usually provided by the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: DbUrl, + /// The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the + /// authenticated End-User. This is defined in the OIDC specification. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub userinfo_endpoint: DbUrl, + /// The OAuth 2.0 claim containing the unique user ID returned by the provider. Usually this + /// should be set to "sub". + pub id_claim: String, + /// The client_id is provided by the OAuth 2.0 provider and is a unique identifier to this + /// service + pub client_id: String, + /// The client_secret is provided by the OAuth 2.0 provider and is used to authenticate this + /// service with the provider + #[serde(skip)] + pub client_secret: SensitiveString, + /// Lists the scopes requested from users. Users will have to grant access to the requested scope + /// at sign up. + pub scopes: String, + /// Automatically sets email as verified on registration + pub auto_verify_email: bool, + /// Allows linking an OAUTH account to an existing user account by matching emails + pub account_linking_enabled: bool, + /// switch to enable or disable an oauth provider + pub enabled: bool, + pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] + pub updated: Option>, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize)] +#[serde(transparent)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// A subset of OAuthProvider used for public requests, for example to display the OAUTH buttons on +// the login page +pub struct PublicOAuthProvider(pub OAuthProvider); + +impl Serialize for PublicOAuthProvider { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("PublicOAuthProvider", 5)?; + state.serialize_field("id", &self.0.id)?; + state.serialize_field("display_name", &self.0.display_name)?; + state.serialize_field("authorization_endpoint", &self.0.authorization_endpoint)?; + state.serialize_field("client_id", &self.0.client_id)?; + state.serialize_field("scopes", &self.0.scopes)?; + state.end() + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] +pub struct OAuthProviderInsertForm { + pub display_name: String, + pub issuer: DbUrl, + pub authorization_endpoint: DbUrl, + pub token_endpoint: DbUrl, + pub userinfo_endpoint: DbUrl, + pub id_claim: String, + pub client_id: String, + pub client_secret: String, + pub scopes: String, + pub auto_verify_email: Option, + pub account_linking_enabled: Option, + pub enabled: Option, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] +pub struct OAuthProviderUpdateForm { + pub display_name: Option, + pub authorization_endpoint: Option, + pub token_endpoint: Option, + pub userinfo_endpoint: Option, + pub id_claim: Option, + pub client_secret: Option, + pub scopes: Option, + pub auto_verify_email: Option, + pub account_linking_enabled: Option, + pub enabled: Option, + 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 541d9c307..bed659a10 100644 --- a/crates/db_schema/src/source/post.rs +++ b/crates/db_schema/src/source/post.rs @@ -1,12 +1,13 @@ 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")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -18,10 +19,11 @@ use typed_builder::TypedBuilder; 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, @@ -30,66 +32,90 @@ 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>, } -#[derive(Debug, Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Debug, Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = post))] pub struct PostInsertForm { - #[builder(!default)] pub name: String, - #[builder(!default)] pub creator_id: PersonId, - #[builder(!default)] pub community_id: CommunityId, + #[new(default)] pub nsfw: Option, + #[new(default)] pub url: Option, + #[new(default)] pub body: Option, + #[new(default)] pub removed: Option, + #[new(default)] pub locked: Option, + #[new(default)] pub updated: Option>, + #[new(default)] pub published: Option>, + #[new(default)] pub deleted: Option, + #[new(default)] pub embed_title: Option, + #[new(default)] pub embed_description: Option, + #[new(default)] pub embed_video_url: Option, + #[new(default)] pub thumbnail_url: Option, + #[new(default)] pub ap_id: Option, + #[new(default)] pub local: Option, + #[new(default)] pub language_id: Option, + #[new(default)] pub featured_community: Option, + #[new(default)] pub featured_local: Option, + #[new(default)] pub url_content_type: Option, + #[new(default)] pub alt_text: Option, + #[new(default)] + pub scheduled_publish_time: Option>, } #[derive(Debug, Clone, Default)] @@ -116,6 +142,7 @@ pub struct PostUpdateForm { pub featured_local: Option, pub url_content_type: Option>, pub alt_text: Option>, + pub scheduled_publish_time: Option>>, } #[derive(PartialEq, Eq, Debug)] @@ -124,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, } @@ -149,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, @@ -171,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, @@ -193,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 94a600921..f15373907 100644 --- a/crates/db_schema/src/source/private_message.rs +++ b/crates/db_schema/src/source/private_message.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -30,27 +29,30 @@ 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, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = private_message))] pub struct PrivateMessageInsertForm { - #[builder(!default)] pub creator_id: PersonId, - #[builder(!default)] pub recipient_id: PersonId, - #[builder(!default)] pub content: String, + #[new(default)] pub deleted: Option, + #[new(default)] pub read: Option, + #[new(default)] pub published: Option>, + #[new(default)] pub updated: Option>, + #[new(default)] pub ap_id: Option, + #[new(default)] pub local: Option, } 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 325bff97c..0fe33de01 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] @@ -22,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, @@ -44,28 +48,37 @@ 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, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = site))] pub struct SiteInsertForm { - #[builder(!default)] pub name: String, - pub sidebar: Option, - pub updated: Option>, - pub icon: Option, - pub banner: Option, - pub description: Option, - pub actor_id: Option, - pub last_refreshed_at: Option>, - pub inbox_url: Option, - pub private_key: Option, - pub public_key: Option, - #[builder(!default)] pub instance_id: InstanceId, + #[new(default)] + pub sidebar: Option, + #[new(default)] + pub updated: Option>, + #[new(default)] + pub icon: Option, + #[new(default)] + pub banner: Option, + #[new(default)] + pub description: Option, + #[new(default)] + pub actor_id: Option, + #[new(default)] + pub last_refreshed_at: Option>, + #[new(default)] + pub inbox_url: Option, + #[new(default)] + pub private_key: Option, + #[new(default)] + pub public_key: Option, + #[new(default)] pub content_warning: Option, } diff --git a/crates/db_schema/src/source/tagline.rs b/crates/db_schema/src/source/tagline.rs index dbc904a78..80c045a0a 100644 --- a/crates/db_schema/src/source/tagline.rs +++ b/crates/db_schema/src/source/tagline.rs @@ -1,4 +1,3 @@ -use crate::newtypes::LocalSiteId; #[cfg(feature = "full")] use crate::schema::tagline; use chrono::{DateTime, Utc}; @@ -9,31 +8,30 @@ use ts_rs::TS; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable, TS) -)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = tagline))] -#[cfg_attr( - feature = "full", - diesel(belongs_to(crate::source::local_site::LocalSite)) -)] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A tagline, shown at the top of your site. pub struct Tagline { pub id: i32, - pub local_site_id: LocalSiteId, pub content: String, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } #[derive(Clone, Default)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = tagline))] -pub struct TaglineForm { - pub local_site_id: LocalSiteId, +pub struct TaglineInsertForm { pub content: String, - pub updated: Option>, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = tagline))] +pub struct TaglineUpdateForm { + pub content: String, + pub updated: DateTime, } diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index 2b0da6c7f..bc30c6fb9 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -1,7 +1,6 @@ use crate::{ - diesel::OptionalExtension, newtypes::{CommunityId, DbUrl, PersonId}, - utils::{get_conn, DbPool}, + utils::{get_conn, uplete, DbPool}, }; use diesel::{ associations::HasTable, @@ -43,10 +42,10 @@ where async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result; - async fn read(pool: &mut DbPool<'_>, id: Self::IdType) -> Result, Error> { + async fn read(pool: &mut DbPool<'_>, id: Self::IdType) -> Result { let query: Find = Self::table().find(id); let conn = &mut *get_conn(pool).await?; - query.first(conn).await.optional() + query.first(conn).await } /// when you want to null out a column, you have to send Some(None)), since sending None means you @@ -77,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; } @@ -88,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; } @@ -104,7 +103,7 @@ pub trait Likeable { pool: &mut DbPool<'_>, person_id: PersonId, item_id: Self::IdType, - ) -> Result + ) -> Result where Self: Sized; } @@ -115,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; } @@ -126,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; } @@ -137,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 8f60b20f1..a96b750b3 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,19 +1,30 @@ -use crate::{newtypes::DbUrl, schema_setup, CommentSortType, SortType}; +pub mod uplete; + +use crate::{newtypes::DbUrl, schema_setup, 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, - OptionalExtension, + JoinOnDsl, + NullableExpressionMethods, + QuerySource, + Table, }; use diesel_async::{ pg::AsyncPgConnection, @@ -23,14 +34,14 @@ 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::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, settings::SETTINGS, - utils::validation::clean_url_params, + utils::validation::clean_url, }; use regex::Regex; use rustls::{ @@ -48,7 +59,7 @@ use rustls::{ }; use std::{ ops::{Deref, DerefMut}, - sync::{Arc, LazyLock}, + sync::{Arc, LazyLock, OnceLock}, time::Duration, }; use tracing::error; @@ -60,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 @@ -288,7 +301,7 @@ pub fn is_email_regex(test: &str) -> bool { EMAIL_REGEX.is_match(test) } -/// Takes an API text input, and converts it to an optional diesel DB update. +/// Takes an API optional text input, and converts it to an optional diesel DB update. pub fn diesel_string_update(opt: Option<&str>) -> Option> { match opt { // An empty string is an erase @@ -298,6 +311,17 @@ pub fn diesel_string_update(opt: Option<&str>) -> Option> { } } +/// Takes an API optional text input, and converts it to an optional diesel DB update (for non +/// nullable properties). +pub fn diesel_required_string_update(opt: Option<&str>) -> Option { + match opt { + // An empty string is no change + Some("") => None, + Some(str) => Some(str.into()), + None => None, + } +} + /// Takes an optional API URL-type input, and converts it to an optional diesel DB update. /// Also cleans the url params. pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult>> { @@ -305,7 +329,20 @@ pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult> // An empty string is an erase Some("") => Ok(Some(None)), Some(str_url) => Url::parse(str_url) - .map(|u| Some(Some(clean_url_params(&u).into()))) + .map(|u| Some(Some(clean_url(&u).into()))) + .with_lemmy_type(LemmyErrorType::InvalidUrl), + None => Ok(None), + } +} + +/// Takes an optional API URL-type input, and converts it to an optional diesel DB update (for non +/// nullable properties). Also cleans the url params. +pub fn diesel_required_url_update(opt: Option<&str>) -> LemmyResult> { + match opt { + // An empty string is no change + Some("") => Ok(None), + Some(str_url) => Url::parse(str_url) + .map(|u| Some(clean_url(&u).into())) .with_lemmy_type(LemmyErrorType::InvalidUrl), None => Ok(None), } @@ -316,16 +353,43 @@ pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult> pub fn diesel_url_create(opt: Option<&str>) -> LemmyResult> { match opt { Some(str_url) => Url::parse(str_url) - .map(|u| Some(clean_url_params(&u).into())) + .map(|u| Some(clean_url(&u).into())) .with_lemmy_type(LemmyErrorType::InvalidUrl), None => Ok(None), } } +/// 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(), } @@ -346,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 @@ -449,31 +495,23 @@ 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 { Utc::now() } -pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType { +pub fn post_to_comment_sort_type(sort: PostSortType) -> CommentSortType { + use PostSortType::*; match sort { - SortType::Active | SortType::Hot | SortType::Scaled => CommentSortType::Hot, - SortType::New | SortType::NewComments | SortType::MostComments => CommentSortType::New, - SortType::Old => CommentSortType::Old, - SortType::Controversial => CommentSortType::Controversial, - SortType::TopHour - | SortType::TopSixHour - | SortType::TopTwelveHour - | SortType::TopDay - | SortType::TopAll - | SortType::TopWeek - | SortType::TopYear - | SortType::TopMonth - | SortType::TopThreeMonths - | SortType::TopSixMonths - | SortType::TopNineMonths => CommentSortType::Top, + Active | Hot | Scaled => CommentSortType::Hot, + New | NewComments | MostComments => CommentSortType::New, + Old => CommentSortType::Old, + Controversial => CommentSortType::Controversial, + TopHour | TopSixHour | TopTwelveHour | TopDay | TopAll | TopWeek | TopYear | TopMonth + | TopThreeMonths | TopSixMonths | TopNineMonths => CommentSortType::Top, } } @@ -483,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"] @@ -506,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*"; @@ -517,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> {} @@ -558,12 +705,12 @@ impl Queries { self, pool: &'a mut DbPool<'_>, args: Args, - ) -> Result, DieselError> + ) -> Result where RF: ReadFn<'a, T, Args>, { let conn = get_conn(pool).await?; - (self.read_fn)(conn, args).await.optional() + (self.read_fn)(conn, args).await } pub async fn list<'a, T, Args>( @@ -580,7 +727,6 @@ impl Queries { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; 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 d7b26a1ed..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) @@ -192,11 +173,11 @@ impl CommentReportView { pool: &mut DbPool<'_>, report_id: CommentReportId, my_person_id: PersonId, - ) -> Result, Error> { + ) -> Result { queries().read(pool, (report_id, my_person_id)).await } - /// Returns the current unresolved post report count for the communities you mod + /// Returns the current unresolved comment report count for the communities you mod pub async fn get_report_count( pool: &mut DbPool<'_>, my_person_id: PersonId, @@ -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)) @@ -259,8 +241,7 @@ impl CommentReportQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{ @@ -284,27 +265,24 @@ mod tests { CommunityVisibility, SubscribedType, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> LemmyResult<()> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_crv"); - let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); + let inserted_timmy = Person::create(pool, &new_person).await?; let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) - .await - .unwrap(); + let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; let timmy_view = LocalUserView { local_user: timmy_local_user, local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), @@ -314,21 +292,20 @@ mod tests { let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_crv"); - let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); + let inserted_sara = Person::create(pool, &new_person_2).await?; // Add a third person, since new ppl can only report something once. let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_crv"); - let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap(); + let inserted_jessica = Person::create(pool, &new_person_3).await?; - let new_community = CommunityInsertForm::builder() - .name("test community crv".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community crv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; // Make timmy a mod let timmy_moderator_form = CommunityModeratorForm { @@ -336,25 +313,22 @@ mod tests { person_id: inserted_timmy.id, }; - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form) - .await - .unwrap(); + let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; - let new_post = PostInsertForm::builder() - .name("A test post crv".into()) - .creator_id(inserted_timmy.id) - .community_id(inserted_community.id) - .build(); + let new_post = PostInsertForm::new( + "A test post crv".into(), + inserted_timmy.id, + inserted_community.id, + ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; - let comment_form = CommentInsertForm::builder() - .content("A test comment 32".into()) - .creator_id(inserted_timmy.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let comment_form = CommentInsertForm::new( + inserted_timmy.id, + inserted_post.id, + "A test comment 32".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; // sara reports let sara_report_form = CommentReportForm { @@ -364,9 +338,7 @@ mod tests { reason: "from sara".into(), }; - let inserted_sara_report = CommentReport::report(pool, &sara_report_form) - .await - .unwrap(); + let inserted_sara_report = CommentReport::report(pool, &sara_report_form).await?; // jessica reports let jessica_report_form = CommentReportForm { @@ -376,20 +348,12 @@ mod tests { reason: "from jessica".into(), }; - let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form) - .await - .unwrap(); + let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; - let agg = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap() - .unwrap(); + let agg = CommentAggregates::read(pool, inserted_comment.id).await?; let read_jessica_report_view = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap() - .unwrap(); + CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; let expected_jessica_report_view = CommentReportView { comment_report: inserted_jessica_report.clone(), comment: inserted_comment.clone(), @@ -409,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, @@ -420,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, @@ -441,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, @@ -464,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, @@ -506,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, @@ -518,8 +479,7 @@ mod tests { // Do a batch read of timmys reports let reports = CommentReportQuery::default() .list(pool, &timmy_view) - .await - .unwrap(); + .await?; assert_eq!( reports, @@ -530,20 +490,14 @@ mod tests { ); // Make sure the counts are correct - let report_count = CommentReportView::get_report_count(pool, inserted_timmy.id, false, None) - .await - .unwrap(); + let report_count = + CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; assert_eq!(2, report_count); // Try to resolve the report - CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap(); + CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id).await?; let read_jessica_report_view_after_resolve = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap() - .unwrap(); + CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; let mut expected_jessica_report_view_after_resolve = expected_jessica_report_view; expected_jessica_report_view_after_resolve @@ -575,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, @@ -593,24 +546,21 @@ mod tests { ..Default::default() } .list(pool, &timmy_view) - .await - .unwrap(); + .await?; assert_eq!(reports_after_resolve[0], expected_sara_report_view); assert_eq!(reports_after_resolve.len(), 1); // Make sure the counts are correct let report_count_after_resolved = - CommentReportView::get_report_count(pool, inserted_timmy.id, false, None) - .await - .unwrap(); + CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; assert_eq!(1, report_count_after_resolved); - Person::delete(pool, inserted_timmy.id).await.unwrap(); - Person::delete(pool, inserted_sara.id).await.unwrap(); - Person::delete(pool, inserted_jessica.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Person::delete(pool, inserted_timmy.id).await?; + Person::delete(pool, inserted_sara.id).await?; + Person::delete(pool, inserted_jessica.id).await?; + Community::delete(pool, inserted_community.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index e2752a0c7..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,93 +13,47 @@ 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, - 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, }; fn queries<'a>() -> Queries< impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>, - impl ListFn<'a, CommentView, CommentQuery<'a>>, + 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: CommentQuery<'a>| async move { + 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,48 +171,38 @@ fn queries<'a>() -> Queries< query = query.filter(post::community_id.eq(community_id)); } - if let Some(listing_type) = options.listing_type { - 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 listing_type { - ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ - ListingType::Local => { - query = query - .filter(community::local.eq(true)) - .filter(community::hidden.eq(false).or(is_subscribed)) - } - 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)), - ), - )); - } + 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)), */ + ListingType::Local => { + query = query + .filter(community::local.eq(true)) + .filter(community::hidden.eq(false).or(is_subscribed)) + } + ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), + ListingType::ModeratorView => { + 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)); } } @@ -278,25 +223,28 @@ 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) { + query = query + .filter(post::nsfw.eq(false)) + .filter(community::nsfw.eq(false)); }; 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() { @@ -364,21 +312,18 @@ impl CommentView { pool: &mut DbPool<'_>, comment_id: CommentId, my_local_user: Option<&'a LocalUser>, - ) -> Result, Error> { + ) -> Result { // If a person is given, then my_vote (res.9), if None, should be 0, not null // Necessary to differentiate between other person's votes - if let Ok(Some(res)) = queries().read(pool, (comment_id, my_local_user)).await { - let mut new_view = res.clone(); - if my_local_user.is_some() && res.my_vote.is_none() { - new_view.my_vote = Some(0); - } - if res.comment.deleted || res.comment.removed { - new_view.comment.content = String::new(); - } - Ok(Some(new_view)) - } else { - Ok(None) + let res = queries().read(pool, (comment_id, my_local_user)).await?; + let mut new_view = res.clone(); + if my_local_user.is_some() && res.my_vote.is_none() { + new_view.my_vote = Some(0); } + if res.comment.deleted || res.comment.removed { + new_view.comment.content = String::new(); + } + Ok(new_view) } } @@ -401,10 +346,10 @@ pub struct CommentQuery<'a> { } impl<'a> CommentQuery<'a> { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { Ok( queries() - .list(pool, self) + .list(pool, (self, site)) .await? .into_iter() .map(|mut c| { @@ -419,7 +364,7 @@ impl<'a> CommentQuery<'a> { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{ @@ -444,6 +389,9 @@ mod tests { }, community::{ Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -457,14 +405,15 @@ mod tests { local_user_vote_display_mode::LocalUserVoteDisplayMode, person::{Person, PersonInsertForm}, person_block::{PersonBlock, PersonBlockForm}, - post::{Post, PostInsertForm}, + 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, }; - use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; @@ -477,6 +426,7 @@ mod tests { timmy_local_user_view: LocalUserView, inserted_sara_person: Person, inserted_community: Community, + site: Site, } async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { @@ -491,23 +441,21 @@ mod tests { let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara"); let inserted_sara_person = Person::create(pool, &sara_person_form).await?; - let new_community = CommunityInsertForm::builder() - .name("test community 5".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community 5".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); let inserted_community = Community::create(pool, &new_community).await?; - let new_post = PostInsertForm::builder() - .name("A test post 2".into()) - .creator_id(inserted_timmy_person.id) - .community_id(inserted_community.id) - .build(); - + let new_post = PostInsertForm::new( + "A test post 2".into(), + inserted_timmy_person.id, + inserted_community.id, + ); let inserted_post = Post::create(pool, &new_post).await?; - let english_id = Language::read_id_from_code(pool, Some("en")).await?; + let english_id = Language::read_id_from_code(pool, "en").await?; // Create a comment tree with this hierarchy // 0 @@ -517,65 +465,70 @@ mod tests { // 3 4 // \ // 5 - let comment_form_0 = CommentInsertForm::builder() - .content("Comment 0".into()) - .creator_id(inserted_timmy_person.id) - .post_id(inserted_post.id) - .language_id(english_id) - .build(); + let comment_form_0 = CommentInsertForm { + language_id: Some(english_id), + ..CommentInsertForm::new( + inserted_timmy_person.id, + inserted_post.id, + "Comment 0".into(), + ) + }; let inserted_comment_0 = Comment::create(pool, &comment_form_0, None).await?; - let comment_form_1 = CommentInsertForm::builder() - .content("Comment 1, A test blocked comment".into()) - .creator_id(inserted_sara_person.id) - .post_id(inserted_post.id) - .language_id(english_id) - .build(); - + let comment_form_1 = CommentInsertForm { + language_id: Some(english_id), + ..CommentInsertForm::new( + inserted_sara_person.id, + inserted_post.id, + "Comment 1, A test blocked comment".into(), + ) + }; let inserted_comment_1 = Comment::create(pool, &comment_form_1, Some(&inserted_comment_0.path)).await?; - let finnish_id = Language::read_id_from_code(pool, Some("fi")).await?; - let comment_form_2 = CommentInsertForm::builder() - .content("Comment 2".into()) - .creator_id(inserted_timmy_person.id) - .post_id(inserted_post.id) - .language_id(finnish_id) - .build(); + let finnish_id = Language::read_id_from_code(pool, "fi").await?; + let comment_form_2 = CommentInsertForm { + language_id: Some(finnish_id), + ..CommentInsertForm::new( + inserted_timmy_person.id, + inserted_post.id, + "Comment 2".into(), + ) + }; let inserted_comment_2 = Comment::create(pool, &comment_form_2, Some(&inserted_comment_0.path)).await?; - let comment_form_3 = CommentInsertForm::builder() - .content("Comment 3".into()) - .creator_id(inserted_timmy_person.id) - .post_id(inserted_post.id) - .language_id(english_id) - .build(); - + let comment_form_3 = CommentInsertForm { + language_id: Some(english_id), + ..CommentInsertForm::new( + inserted_timmy_person.id, + inserted_post.id, + "Comment 3".into(), + ) + }; let _inserted_comment_3 = Comment::create(pool, &comment_form_3, Some(&inserted_comment_1.path)).await?; - let polish_id = Language::read_id_from_code(pool, Some("pl")) - .await? - .ok_or(LemmyErrorType::LanguageNotAllowed)?; - let comment_form_4 = CommentInsertForm::builder() - .content("Comment 4".into()) - .creator_id(inserted_timmy_person.id) - .post_id(inserted_post.id) - .language_id(Some(polish_id)) - .build(); + let polish_id = Language::read_id_from_code(pool, "pl").await?; + let comment_form_4 = CommentInsertForm { + language_id: Some(polish_id), + ..CommentInsertForm::new( + inserted_timmy_person.id, + inserted_post.id, + "Comment 4".into(), + ) + }; let inserted_comment_4 = Comment::create(pool, &comment_form_4, Some(&inserted_comment_1.path)).await?; - let comment_form_5 = CommentInsertForm::builder() - .content("Comment 5".into()) - .creator_id(inserted_timmy_person.id) - .post_id(inserted_post.id) - .build(); - + let comment_form_5 = CommentInsertForm::new( + inserted_timmy_person.id, + inserted_post.id, + "Comment 5".into(), + ); let _inserted_comment_5 = Comment::create(pool, &comment_form_5, Some(&inserted_comment_4.path)).await?; @@ -595,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, }; @@ -608,6 +560,8 @@ mod tests { person: inserted_timmy_person.clone(), counts: Default::default(), }; + let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id); + let site = Site::create(pool, &site_form).await?; Ok(Data { inserted_instance, inserted_comment_0, @@ -617,13 +571,14 @@ mod tests { timmy_local_user_view, inserted_sara_person, inserted_community, + site, }) } #[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?; @@ -637,14 +592,12 @@ mod tests { post_id: (Some(data.inserted_post.id)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!( - &expected_comment_view_no_person, - read_comment_views_no_person - .first() - .ok_or(LemmyErrorType::CouldntFindComment)? + Some(&expected_comment_view_no_person), + read_comment_views_no_person.first() ); let read_comment_views_with_person = CommentQuery { @@ -653,7 +606,7 @@ mod tests { local_user: (Some(&data.timmy_local_user_view.local_user)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!( @@ -669,8 +622,7 @@ mod tests { data.inserted_comment_1.id, Some(&data.timmy_local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; // Make sure block set the creator blocked assert!(read_comment_from_blocked_person.creator_blocked); @@ -681,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?; @@ -695,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, }; @@ -706,7 +657,7 @@ mod tests { liked_only: Some(true), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await? .into_iter() .map(|c| c.comment.content) @@ -722,7 +673,7 @@ mod tests { disliked_only: Some(true), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert!(read_disliked_comment_views.is_empty()); @@ -733,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?; @@ -743,7 +694,7 @@ mod tests { parent_path: (Some(top_path)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; let child_path = data.inserted_comment_1.path.clone(); @@ -752,7 +703,7 @@ mod tests { parent_path: (Some(child_path)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; // Make sure the comment parent-limited fetch is correct @@ -772,7 +723,7 @@ mod tests { max_depth: (Some(1)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; // Make sure a depth limited one only has the top comment @@ -790,7 +741,7 @@ mod tests { sort: (Some(CommentSortType::New)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; // Make sure a depth limited one, and given child comment 1, has 3 @@ -806,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?; @@ -816,14 +767,12 @@ mod tests { local_user: (Some(&data.timmy_local_user_view.local_user)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_length!(5, all_languages); // change user lang to finnish, should only show one post in finnish and one undetermined - let finnish_id = Language::read_id_from_code(pool, Some("fi")) - .await? - .ok_or(LemmyErrorType::LanguageNotAllowed)?; + let finnish_id = Language::read_id_from_code(pool, "fi").await?; LocalUserLanguage::update( pool, vec![finnish_id], @@ -834,7 +783,7 @@ mod tests { local_user: (Some(&data.timmy_local_user_view.local_user)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_length!(2, finnish_comments); let finnish_comment = finnish_comments @@ -842,11 +791,8 @@ mod tests { .find(|c| c.comment.language_id == finnish_id); assert!(finnish_comment.is_some()); assert_eq!( - data.inserted_comment_2.content, - finnish_comment - .ok_or(LemmyErrorType::CouldntFindComment)? - .comment - .content + Some(&data.inserted_comment_2.content), + finnish_comment.map(|c| &c.comment.content) ); // now show all comments with undetermined language (which is the default value) @@ -860,7 +806,7 @@ mod tests { local_user: (Some(&data.timmy_local_user_view.local_user)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_length!(1, undetermined_comment); @@ -870,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?; @@ -884,7 +830,7 @@ mod tests { post_id: Some(data.inserted_comment_2.post_id), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); assert!(comments[0].comment.distinguished); @@ -895,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?; @@ -913,7 +859,7 @@ mod tests { sort: (Some(CommentSortType::Old)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(comments[1].creator.name, "sara"); @@ -926,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?; @@ -934,7 +880,7 @@ mod tests { sort: (Some(CommentSortType::Old)), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; // Timmy is an admin, and make sure that field is true @@ -951,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?; @@ -974,7 +920,7 @@ mod tests { saved_only: Some(true), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; // There should only be two comments @@ -1004,14 +950,13 @@ mod tests { LocalUser::delete(pool, data.timmy_local_user_view.local_user.id).await?; Person::delete(pool, data.inserted_sara_person.id).await?; Instance::delete(pool, data.inserted_instance.id).await?; + Site::delete(pool, data.site.id).await?; Ok(()) } async fn expected_comment_view(data: &Data, pool: &mut DbPool<'_>) -> LemmyResult { - let agg = CommentAggregates::read(pool, data.inserted_comment_0.id) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let agg = CommentAggregates::read(pool, data.inserted_comment_0.id).await?; Ok(CommentView { creator_banned_from_community: false, banned_from_community: false, @@ -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, @@ -1083,6 +1027,7 @@ mod tests { featured_community: false, featured_local: false, url_content_type: None, + scheduled_publish_time: None, }, community: Community { id: data.inserted_community.id, @@ -1094,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, @@ -1106,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, @@ -1127,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?; @@ -1144,7 +1089,7 @@ mod tests { let unauthenticated_query = CommentQuery { ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(0, unauthenticated_query.len()); @@ -1152,12 +1097,12 @@ mod tests { local_user: Some(&data.timmy_local_user_view.local_user), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(5, authenticated_query.len()); - let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await?; - assert!(unauthenticated_comment.is_none()); + let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await; + assert!(unauthenticated_comment.is_err()); let authenticated_comment = CommentView::read( pool, @@ -1173,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?; @@ -1204,8 +1149,7 @@ mod tests { data.inserted_comment_0.id, Some(&inserted_banned_from_comm_local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; assert!(comment_view.banned_from_community); @@ -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?; @@ -1225,11 +1169,131 @@ mod tests { data.inserted_comment_0.id, Some(&data.timmy_local_user_view.local_user), ) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + .await?; assert!(!comment_view.banned_from_community); cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn comment_listings_hide_nsfw() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Mark a post as nsfw + let update_form = PostUpdateForm { + nsfw: Some(true), + ..Default::default() + }; + Post::update(pool, data.inserted_post.id, &update_form).await?; + + // Make sure comments of this post are not returned + let comments = CommentQuery::default().list(&data.site, pool).await?; + assert_eq!(0, comments.len()); + + // Mark site as nsfw + let mut site = data.site.clone(); + site.content_warning = Some("nsfw".to_string()); + + // Now comments of nsfw post are returned + let comments = CommentQuery::default().list(&site, pool).await?; + assert_eq!(6, comments.len()); + + 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 4d2f1fd85..606e807e9 100644 --- a/crates/db_views/src/custom_emoji_view.rs +++ b/crates/db_views/src/custom_emoji_view.rs @@ -2,10 +2,10 @@ use crate::structs::CustomEmojiView; use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - newtypes::{CustomEmojiId, LocalSiteId}, + newtypes::CustomEmojiId, schema::{custom_emoji, custom_emoji_keyword}, source::{custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword}, - utils::{get_conn, DbPool}, + utils::{get_conn, limit_and_offset, DbPool}, }; use std::collections::HashMap; @@ -35,18 +35,34 @@ impl CustomEmojiView { } } - pub async fn get_all( + pub async fn list( pool: &mut DbPool<'_>, - for_local_site_id: LocalSiteId, + category: &Option, + page: Option, + limit: Option, + ignore_page_limits: bool, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let emojis = custom_emoji::table - .filter(custom_emoji::local_site_id.eq(for_local_site_id)) + + let mut query = custom_emoji::table .left_join( custom_emoji_keyword::table.on(custom_emoji_keyword::custom_emoji_id.eq(custom_emoji::id)), ) .order(custom_emoji::category) - .then_order_by(custom_emoji::id) + .into_boxed(); + + if !ignore_page_limits { + let (limit, offset) = limit_and_offset(page, limit)?; + query = query.limit(limit).offset(offset); + } + + if let Some(category) = category { + query = query.filter(custom_emoji::category.eq(category)) + } + + query = query.then_order_by(custom_emoji::id); + + let emojis = query .select(( custom_emoji::all_columns, custom_emoji_keyword::all_columns.nullable(), // (or all the columns if you want) @@ -60,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 0c13b0a68..8d55b96fe 100644 --- a/crates/db_views/src/local_user_view.rs +++ b/crates/db_views/src/local_user_view.rs @@ -3,8 +3,14 @@ use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest}; use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - newtypes::{LocalUserId, PersonId}, - schema::{local_user, local_user_vote_display_mode, person, person_aggregates}, + 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, @@ -23,6 +29,7 @@ enum ReadBy<'a> { Name(&'a str), NameOrEmail(&'a str), Email(&'a str), + OAuthID(OAuthProviderId, &'a str), } enum ListMode { @@ -58,12 +65,21 @@ fn queries<'a>( ), _ => query, }; - query + let query = query .inner_join(local_user_vote_display_mode::table) - .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))) - .select(selection) - .first(&mut conn) - .await + .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))); + + if let ReadBy::OAuthID(oauth_provider_id, oauth_user_id) = search { + query + .inner_join(oauth_account::table) + .filter(oauth_account::oauth_provider_id.eq(oauth_provider_id)) + .filter(oauth_account::oauth_user_id.eq(oauth_user_id)) + .select(selection) + .first(&mut conn) + .await + } else { + query.select(selection).first(&mut conn).await + } }; let list = move |mut conn: DbConn<'a>, mode: ListMode| async move { @@ -86,43 +102,69 @@ fn queries<'a>( } impl LocalUserView { - pub async fn read( - pool: &mut DbPool<'_>, - local_user_id: LocalUserId, - ) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, local_user_id: LocalUserId) -> Result { queries().read(pool, ReadBy::Id(local_user_id)).await } - pub async fn read_person( - pool: &mut DbPool<'_>, - person_id: PersonId, - ) -> Result, Error> { + pub async fn read_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { queries().read(pool, ReadBy::Person(person_id)).await } - pub async fn read_from_name(pool: &mut DbPool<'_>, name: &str) -> Result, Error> { + pub async fn read_from_name(pool: &mut DbPool<'_>, name: &str) -> Result { queries().read(pool, ReadBy::Name(name)).await } pub async fn find_by_email_or_name( pool: &mut DbPool<'_>, name_or_email: &str, - ) -> Result, Error> { + ) -> Result { queries() .read(pool, ReadBy::NameOrEmail(name_or_email)) .await } - pub async fn find_by_email( - pool: &mut DbPool<'_>, - from_email: &str, - ) -> Result, Error> { + pub async fn find_by_email(pool: &mut DbPool<'_>, from_email: &str) -> Result { queries().read(pool, ReadBy::Email(from_email)).await } + pub async fn find_by_oauth_id( + pool: &mut DbPool<'_>, + oauth_provider_id: OAuthProviderId, + oauth_user_id: &str, + ) -> Result { + queries() + .read(pool, ReadBy::OAuthID(oauth_provider_id, oauth_user_id)) + .await + } + 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 0cd06dd4e..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) @@ -220,7 +159,7 @@ impl PostReportView { pool: &mut DbPool<'_>, report_id: PostReportId, my_person_id: PersonId, - ) -> Result, Error> { + ) -> Result { queries().read(pool, (report_id, my_person_id)).await } @@ -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)) @@ -284,8 +224,7 @@ impl PostReportQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{ @@ -306,27 +245,24 @@ mod tests { traits::{Crud, Joinable, Reportable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> LemmyResult<()> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_prv"); - let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); + let inserted_timmy = Person::create(pool, &new_person).await?; let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) - .await - .unwrap(); + let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; let timmy_view = LocalUserView { local_user: timmy_local_user, local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), @@ -336,21 +272,20 @@ mod tests { let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_prv"); - let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); + let inserted_sara = Person::create(pool, &new_person_2).await?; // Add a third person, since new ppl can only report something once. let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_prv"); - let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap(); + let inserted_jessica = Person::create(pool, &new_person_3).await?; - let new_community = CommunityInsertForm::builder() - .name("test community prv".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community prv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; // Make timmy a mod let timmy_moderator_form = CommunityModeratorForm { @@ -358,17 +293,14 @@ mod tests { person_id: inserted_timmy.id, }; - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form) - .await - .unwrap(); + let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; - let new_post = PostInsertForm::builder() - .name("A test post crv".into()) - .creator_id(inserted_timmy.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let new_post = PostInsertForm::new( + "A test post crv".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; // sara reports let sara_report_form = PostReportForm { @@ -380,15 +312,14 @@ mod tests { reason: "from sara".into(), }; - PostReport::report(pool, &sara_report_form).await.unwrap(); + PostReport::report(pool, &sara_report_form).await?; - let new_post_2 = PostInsertForm::builder() - .name("A test post crv 2".into()) - .creator_id(inserted_timmy.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post_2 = Post::create(pool, &new_post_2).await.unwrap(); + let new_post_2 = PostInsertForm::new( + "A test post crv 2".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post_2 = Post::create(pool, &new_post_2).await?; // jessica reports let jessica_report_form = PostReportForm { @@ -400,15 +331,10 @@ mod tests { reason: "from jessica".into(), }; - let inserted_jessica_report = PostReport::report(pool, &jessica_report_form) - .await - .unwrap(); + let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; let read_jessica_report_view = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap() - .unwrap(); + PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; assert_eq!( read_jessica_report_view.post_report, @@ -422,31 +348,23 @@ mod tests { assert_eq!(read_jessica_report_view.resolver, None); // Do a batch read of timmys reports - let reports = PostReportQuery::default() - .list(pool, &timmy_view) - .await - .unwrap(); + let reports = PostReportQuery::default().list(pool, &timmy_view).await?; assert_eq!(reports[1].creator.id, inserted_sara.id); assert_eq!(reports[0].creator.id, inserted_jessica.id); // Make sure the counts are correct - let report_count = PostReportView::get_report_count(pool, inserted_timmy.id, false, None) - .await - .unwrap(); + let report_count = + PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; assert_eq!(2, report_count); // Pretend the post was removed, and resolve all reports for that object. // This is called manually in the API for post removals PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, inserted_timmy.id) - .await - .unwrap(); + .await?; let read_jessica_report_view_after_resolve = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap() - .unwrap(); + PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; assert!(read_jessica_report_view_after_resolve.post_report.resolved); assert_eq!( read_jessica_report_view_after_resolve @@ -455,8 +373,10 @@ mod tests { Some(inserted_timmy.id) ); assert_eq!( - read_jessica_report_view_after_resolve.resolver.unwrap().id, - inserted_timmy.id + read_jessica_report_view_after_resolve + .resolver + .map(|r| r.id), + Some(inserted_timmy.id) ); // Do a batch read of timmys reports @@ -466,24 +386,21 @@ mod tests { ..Default::default() } .list(pool, &timmy_view) - .await - .unwrap(); + .await?; assert_length!(1, reports_after_resolve); assert_eq!(reports_after_resolve[0].creator.id, inserted_sara.id); // Make sure the counts are correct let report_count_after_resolved = - PostReportView::get_report_count(pool, inserted_timmy.id, false, None) - .await - .unwrap(); + PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; assert_eq!(1, report_count_after_resolved); - Person::delete(pool, inserted_timmy.id).await.unwrap(); - Person::delete(pool, inserted_sara.id).await.unwrap(); - Person::delete(pool, inserted_jessica.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Person::delete(pool, inserted_timmy.id).await?; + Person::delete(pool, inserted_sara.id).await?; + Person::delete(pool, inserted_jessica.id).await?; + Community::delete(pool, inserted_community.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 0ec7e0a5d..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,41 +55,17 @@ use lemmy_db_schema::{ ReadFn, ReverseTimestampKey, }, + CommunityVisibility, ListingType, - SortType, + PostSortType, }; use tracing::debug; +use PostSortType::*; 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 @@ -100,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, ), )) @@ -290,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)), ); } @@ -303,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() @@ -317,11 +208,18 @@ fn queries<'a>() -> Queries< // hide posts from deleted communities query = query.filter(community::deleted.eq(false)); - // only show deleted posts to creator + // only creator can see deleted posts and unpublished scheduled posts if let Some(person_id) = options.local_user.person_id() { query = query.filter(post::deleted.eq(false).or(post::creator_id.eq(person_id))); + query = query.filter( + post::scheduled_publish_time + .is_null() + .or(post::creator_id.eq(person_id)), + ); } else { - query = query.filter(post::deleted.eq(false)); + query = query + .filter(post::deleted.eq(false)) + .filter(post::scheduled_publish_time.is_null()); } // only show removed posts to admin when viewing user profile @@ -338,62 +236,34 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::creator_id.eq(creator_id)); } - if let Some(listing_type) = options.listing_type { - if let Some(person_id) = options.local_user.person_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)), - ), - ); - match listing_type { - ListingType::Subscribed => query = query.filter(is_subscribed), - ListingType::Local => { - query = query - .filter(community::local.eq(true)) - .filter(community::hidden.eq(false).or(is_subscribed)); - } - 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)), - ), - )); - } - } + 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 => { + query = query + .filter(community::local.eq(true)) + .filter(community::hidden.eq(false).or(is_subscribed)); } - // If your person_id is missing, only show local - else { - match listing_type { - ListingType::Local => { - query = query - .filter(community::local.eq(true)) - .filter(community::hidden.eq(false)); - } - _ => query = query.filter(community::hidden.eq(false)), - } + ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), + ListingType::ModeratorView => { + query = query.filter(community_actions::became_moderator.is_not_null()); } - } else { - query = query.filter(community::hidden.eq(false)); - } - - if let Some(url_search) = &options.url_search { - query = query.filter(post::url.eq(url_search)); } if let Some(search_term) = &options.search_term { - let searcher = fuzzy_search(search_term); - query = query - .filter( - post::name - .ilike(searcher.clone()) - .or(post::body.ilike(searcher)), - ) + if options.url_only.unwrap_or_default() { + query = query.filter(post::url.eq(search_term)); + } else { + let searcher = fuzzy_search(search_term); + let name_filter = post::name.ilike(searcher.clone()); + let body_filter = post::body.ilike(searcher.clone()); + query = if options.title_only.unwrap_or_default() { + query.filter(name_filter) + } else { + query.filter(name_filter.or(body_filter)) + } .filter(not(post::removed.or(post::deleted))); + } } if !options @@ -409,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. @@ -423,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)?; @@ -507,33 +379,33 @@ fn queries<'a>() -> Queries< let time = |interval| post_aggregates::published.gt(now() - interval); // then use the main sort - query = match options.sort.unwrap_or(SortType::Hot) { - SortType::Active => query.then_desc(key::hot_rank_active), - SortType::Hot => query.then_desc(key::hot_rank), - SortType::Scaled => query.then_desc(key::scaled_rank), - SortType::Controversial => query.then_desc(key::controversy_rank), - SortType::New => query.then_desc(key::published), - SortType::Old => query.then_desc(ReverseTimestampKey(key::published)), - SortType::NewComments => query.then_desc(key::newest_comment_time), - SortType::MostComments => query.then_desc(key::comments), - SortType::TopAll => query.then_desc(key::score), - SortType::TopYear => query.then_desc(key::score).filter(time(1.years())), - SortType::TopMonth => query.then_desc(key::score).filter(time(1.months())), - SortType::TopWeek => query.then_desc(key::score).filter(time(1.weeks())), - SortType::TopDay => query.then_desc(key::score).filter(time(1.days())), - SortType::TopHour => query.then_desc(key::score).filter(time(1.hours())), - SortType::TopSixHour => query.then_desc(key::score).filter(time(6.hours())), - SortType::TopTwelveHour => query.then_desc(key::score).filter(time(12.hours())), - SortType::TopThreeMonths => query.then_desc(key::score).filter(time(3.months())), - SortType::TopSixMonths => query.then_desc(key::score).filter(time(6.months())), - SortType::TopNineMonths => query.then_desc(key::score).filter(time(9.months())), + query = match options.sort.unwrap_or(Hot) { + Active => query.then_desc(key::hot_rank_active), + Hot => query.then_desc(key::hot_rank), + Scaled => query.then_desc(key::scaled_rank), + Controversial => query.then_desc(key::controversy_rank), + New => query.then_desc(key::published), + Old => query.then_desc(ReverseTimestampKey(key::published)), + NewComments => query.then_desc(key::newest_comment_time), + MostComments => query.then_desc(key::comments), + TopAll => query.then_desc(key::score), + TopYear => query.then_desc(key::score).filter(time(1.years())), + TopMonth => query.then_desc(key::score).filter(time(1.months())), + TopWeek => query.then_desc(key::score).filter(time(1.weeks())), + TopDay => query.then_desc(key::score).filter(time(1.days())), + TopHour => query.then_desc(key::score).filter(time(1.hours())), + TopSixHour => query.then_desc(key::score).filter(time(6.hours())), + TopTwelveHour => query.then_desc(key::score).filter(time(12.hours())), + TopThreeMonths => query.then_desc(key::score).filter(time(3.months())), + TopSixMonths => query.then_desc(key::score).filter(time(6.months())), + TopNineMonths => query.then_desc(key::score).filter(time(9.months())), }; // use publish as fallback. especially useful for hot rank which reaches zero after some days. // necessary because old posts can be fetched over federation and inserted with high post id - query = match options.sort.unwrap_or(SortType::Hot) { + query = match options.sort.unwrap_or(Hot) { // A second time-based sort would not be very useful - SortType::New | SortType::Old | SortType::NewComments => query, + New | Old | NewComments => query, _ => query.then_desc(key::published), }; @@ -564,7 +436,7 @@ impl PostView { post_id: PostId, my_local_user: Option<&'a LocalUser>, is_mod_or_admin: bool, - ) -> Result, Error> { + ) -> Result { queries() .read(pool, (post_id, my_local_user, is_mod_or_admin)) .await @@ -589,8 +461,7 @@ impl PaginationCursor { .ok_or_else(err_msg)?, ), ) - .await? - .ok_or_else(err_msg)?; + .await?; Ok(PaginationCursorData(token)) } @@ -605,7 +476,7 @@ pub struct PaginationCursorData(PostAggregates); #[derive(Clone, Default)] pub struct PostQuery<'a> { pub listing_type: Option, - pub sort: Option, + pub sort: Option, pub creator_id: Option, pub community_id: Option, // if true, the query should be handled as if community_id was not given except adding the @@ -613,10 +484,11 @@ pub struct PostQuery<'a> { pub community_id_just_for_prefetch: bool, pub local_user: Option<&'a LocalUser>, pub search_term: Option, - pub url_search: Option, + pub url_only: Option, pub saved_only: Option, pub liked_only: Option, pub disliked_only: Option, + pub title_only: Option, pub page: Option, pub limit: Option, pub page_after: Option, @@ -625,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> { @@ -644,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() { @@ -661,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) @@ -736,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, @@ -745,6 +616,9 @@ mod tests { comment::{Comment, CommentInsertForm}, community::{ Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -760,21 +634,35 @@ mod tests { local_user_vote_display_mode::LocalUserVoteDisplayMode, person::{Person, PersonInsertForm}, person_block::{PersonBlock, PersonBlockForm}, - post::{Post, PostHide, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm}, + post::{ + Post, + PostHide, + PostInsertForm, + PostLike, + PostLikeForm, + PostRead, + PostSaved, + PostSavedForm, + PostUpdateForm, + }, site::Site, }, - traits::{Bannable, Blockable, Crud, Joinable, Likeable}, - utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, + utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, CommunityVisibility, - SortType, + PostSortType, SubscribedType, }; - use lemmy_utils::error::{LemmyErrorType, LemmyResult}; + 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"; const POST_BY_BLOCKED_PERSON: &str = "post by blocked person"; const POST_BY_BOT: &str = "post by bot"; const POST: &str = "post"; @@ -797,7 +685,7 @@ mod tests { impl Data { fn default_post_query(&self) -> PostQuery<'_> { PostQuery { - sort: Some(SortType::New), + sort: Some(PostSortType::New), local_user: Some(&self.local_user_view.local_user), ..Default::default() } @@ -824,13 +712,12 @@ mod tests { let inserted_bot = Person::create(pool, &new_bot).await?; - let new_community = CommunityInsertForm::builder() - .name("test_community_3".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test_community_3".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); let inserted_community = Community::create(pool, &new_community).await?; // Test a person block, make sure the post query doesn't include their post @@ -845,13 +732,14 @@ mod tests { ) .await?; - let post_from_blocked_person = PostInsertForm::builder() - .name(POST_BY_BLOCKED_PERSON.to_string()) - .creator_id(inserted_blocked_person.id) - .community_id(inserted_community.id) - .language_id(Some(LanguageId(1))) - .build(); - + let post_from_blocked_person = PostInsertForm { + language_id: Some(LanguageId(1)), + ..PostInsertForm::new( + POST_BY_BLOCKED_PERSON.to_string(), + inserted_blocked_person.id, + inserted_community.id, + ) + }; Post::create(pool, &post_from_blocked_person).await?; // block that person @@ -863,22 +751,19 @@ mod tests { PersonBlock::block(pool, &person_block).await?; // A sample post - let new_post = PostInsertForm::builder() - .name(POST.to_string()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .language_id(Some(LanguageId(47))) - .build(); - + let new_post = PostInsertForm { + language_id: Some(LanguageId(47)), + ..PostInsertForm::new(POST.to_string(), inserted_person.id, inserted_community.id) + }; let inserted_post = Post::create(pool, &new_post).await?; - let new_bot_post = PostInsertForm::builder() - .name(POST_BY_BOT.to_string()) - .creator_id(inserted_bot.id) - .community_id(inserted_community.id) - .build(); - + let new_bot_post = PostInsertForm::new( + POST_BY_BOT.to_string(), + inserted_bot.id, + inserted_community.id, + ); let inserted_bot_post = Post::create(pool, &new_bot_post).await?; + let local_user_view = LocalUserView { local_user: inserted_local_user, local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), @@ -925,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?; @@ -949,8 +834,7 @@ mod tests { Some(&data.local_user_view.local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; let expected_post_listing_with_user = expected_post_view(&data, pool).await?; @@ -986,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?; @@ -999,9 +883,7 @@ mod tests { .await?; let read_post_listing_single_no_person = - PostView::read(pool, data.inserted_post.id, None, false) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + PostView::read(pool, data.inserted_post.id, None, false).await?; let expected_post_listing_no_person = expected_post_view(&data, pool).await?; @@ -1023,10 +905,69 @@ mod tests { cleanup(data, pool).await } + #[tokio::test] + #[serial] + async fn post_listing_title_only() -> LemmyResult<()> { + let pool = &build_db_pool()?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // A post which contains the search them 'Post' not in the title (but in the body) + let new_post = PostInsertForm { + language_id: Some(LanguageId(47)), + body: Some("Post".to_string()), + ..PostInsertForm::new( + POST_WITH_ANOTHER_TITLE.to_string(), + data.local_user_view.person.id, + data.inserted_community.id, + ) + }; + + let inserted_post = Post::create(pool, &new_post).await?; + + let read_post_listing_by_title_only = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: None, + search_term: Some("Post".to_string()), + title_only: Some(true), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: None, + search_term: Some("Post".to_string()), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + + // Should be 4 posts when we do not search for title only + assert_eq!( + vec![ + POST_WITH_ANOTHER_TITLE, + POST_BY_BOT, + POST, + POST_BY_BLOCKED_PERSON + ], + names(&read_post_listing) + ); + + // Should be 3 posts when we search for title only + assert_eq!( + vec![POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], + names(&read_post_listing_by_title_only) + ); + Post::delete(pool, inserted_post.id).await?; + cleanup(data, pool).await + } + #[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?; @@ -1052,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?; @@ -1078,8 +1019,7 @@ mod tests { Some(&data.local_user_view.local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; let mut expected_post_with_upvote = expected_post_view(&data, pool).await?; expected_post_with_upvote.my_vote = Some(1); @@ -1104,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?; @@ -1157,10 +1097,40 @@ mod tests { cleanup(data, pool).await } + #[tokio::test] + #[serial] + async fn post_listing_saved_only() -> LemmyResult<()> { + let pool = &build_db_pool()?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Save only the bot post + // The saved_only should only show the bot post + let post_save_form = PostSavedForm { + post_id: data.inserted_bot_post.id, + person_id: data.local_user_view.person.id, + }; + PostSaved::save(pool, &post_save_form).await?; + + // Read the saved only + let read_saved_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + saved_only: Some(true), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + + // This should only include the bot post, not the one you created + assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); + + cleanup(data, pool).await + } + #[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?; @@ -1198,25 +1168,22 @@ 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?; - let spanish_id = Language::read_id_from_code(pool, Some("es")) - .await? - .expect("spanish should exist"); + let spanish_id = Language::read_id_from_code(pool, "es").await?; - let french_id = Language::read_id_from_code(pool, Some("fr")) - .await? - .expect("french should exist"); - - let post_spanish = PostInsertForm::builder() - .name(EL_POSTO.to_string()) - .creator_id(data.local_user_view.person.id) - .community_id(data.inserted_community.id) - .language_id(Some(spanish_id)) - .build(); + let french_id = Language::read_id_from_code(pool, "fr").await?; + let post_spanish = PostInsertForm { + language_id: Some(spanish_id), + ..PostInsertForm::new( + EL_POSTO.to_string(), + data.local_user_view.person.id, + data.inserted_community.id, + ) + }; Post::create(pool, &post_spanish).await?; let post_listings_all = data.default_post_query().list(&data.site, pool).await?; @@ -1262,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?; @@ -1297,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?; @@ -1333,32 +1300,69 @@ mod tests { cleanup(data, pool).await } + #[tokio::test] + #[serial] + async fn post_listings_hidden_community() -> LemmyResult<()> { + let pool = &build_db_pool()?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + Community::update( + pool, + data.inserted_community.id, + &CommunityUpdateForm { + hidden: Some(true), + ..Default::default() + }, + ) + .await?; + + let posts = PostQuery::default().list(&data.site, pool).await?; + assert!(posts.is_empty()); + + let posts = data.default_post_query().list(&data.site, pool).await?; + assert!(posts.is_empty()); + + // Follow the community + let form = CommunityFollowerForm { + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(data.inserted_community.id, data.local_user_view.person.id) + }; + CommunityFollower::follow(pool, &form).await?; + + let posts = data.default_post_query().list(&data.site, pool).await?; + assert!(!posts.is_empty()); + + cleanup(data, pool).await + } + #[tokio::test] #[serial] 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?; let blocked_instance = Instance::read_or_create(pool, "another_domain.tld".to_string()).await?; - let community_form = CommunityInsertForm::builder() - .name("test_community_4".to_string()) - .title("none".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(blocked_instance.id) - .build(); + let community_form = CommunityInsertForm::new( + blocked_instance.id, + "test_community_4".to_string(), + "none".to_owned(), + "pubkey".to_string(), + ); let inserted_community = Community::create(pool, &community_form).await?; - let post_form = PostInsertForm::builder() - .name(POST_FROM_BLOCKED_INSTANCE.to_string()) - .creator_id(data.inserted_bot.id) - .community_id(inserted_community.id) - .language_id(Some(LanguageId(1))) - .build(); - + let post_form = PostInsertForm { + language_id: Some(LanguageId(1)), + ..PostInsertForm::new( + POST_FROM_BLOCKED_INSTANCE.to_string(), + data.inserted_bot.id, + inserted_community.id, + ) + }; let post_from_blocked_instance = Post::create(pool, &post_form).await?; // no instance block, should return all posts @@ -1397,16 +1401,16 @@ 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?; - let community_form = CommunityInsertForm::builder() - .name("yes".to_string()) - .title("yes".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(data.inserted_instance.id) - .build(); + let community_form = CommunityInsertForm::new( + data.inserted_instance.id, + "yes".to_string(), + "yes".to_owned(), + "pubkey".to_string(), + ); let inserted_community = Community::create(pool, &community_form).await?; let mut inserted_post_ids = vec![]; @@ -1416,23 +1420,25 @@ mod tests { // and featured for comments in 0..10 { for _ in 0..15 { - let post_form = PostInsertForm::builder() - .name("keep Christ in Christmas".to_owned()) - .creator_id(data.local_user_view.person.id) - .community_id(inserted_community.id) - .featured_local(Some((comments % 2) == 0)) - .featured_community(Some((comments % 2) == 0)) - .published(Some(Utc::now() - Duration::from_secs(comments % 3))) - .build(); + let post_form = PostInsertForm { + featured_local: Some((comments % 2) == 0), + featured_community: Some((comments % 2) == 0), + published: Some(Utc::now() - Duration::from_secs(comments % 3)), + ..PostInsertForm::new( + "keep Christ in Christmas".to_owned(), + data.local_user_view.person.id, + inserted_community.id, + ) + }; let inserted_post = Post::create(pool, &post_form).await?; inserted_post_ids.push(inserted_post.id); for _ in 0..comments { - let comment_form = CommentInsertForm::builder() - .creator_id(data.local_user_view.person.id) - .post_id(inserted_post.id) - .content("yes".to_owned()) - .build(); + let comment_form = CommentInsertForm::new( + data.local_user_view.person.id, + inserted_post.id, + "yes".to_owned(), + ); let inserted_comment = Comment::create(pool, &comment_form, None).await?; inserted_comment_ids.push(inserted_comment.id); } @@ -1441,7 +1447,7 @@ mod tests { let options = PostQuery { community_id: Some(inserted_community.id), - sort: Some(SortType::MostComments), + sort: Some(PostSortType::MostComments), limit: Some(10), ..Default::default() }; @@ -1505,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?; @@ -1555,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?; @@ -1573,7 +1579,7 @@ mod tests { // Make sure it does come back with the show_hidden option let post_listings_show_hidden = PostQuery { - sort: Some(SortType::New), + sort: Some(PostSortType::New), local_user: Some(&data.local_user_view.local_user), show_hidden: Some(true), ..Default::default() @@ -1583,12 +1589,7 @@ mod tests { assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_hidden)); // Make sure that hidden field is true. - assert!( - &post_listings_show_hidden - .first() - .ok_or(LemmyErrorType::CouldntFindPost)? - .hidden - ); + assert!(&post_listings_show_hidden.first().is_some_and(|p| p.hidden)); cleanup(data, pool).await } @@ -1596,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?; @@ -1614,7 +1615,7 @@ mod tests { // Make sure it does come back with the show_nsfw option let post_listings_show_nsfw = PostQuery { - sort: Some(SortType::New), + sort: Some(PostSortType::New), show_nsfw: Some(true), local_user: Some(&data.local_user_view.local_user), ..Default::default() @@ -1624,13 +1625,7 @@ mod tests { assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_nsfw)); // Make sure that nsfw field is true. - assert!( - &post_listings_show_nsfw - .first() - .ok_or(LemmyErrorType::CouldntFindPost)? - .post - .nsfw - ); + assert!(&post_listings_show_nsfw.first().is_some_and(|p| p.post.nsfw)); cleanup(data, pool).await } @@ -1653,9 +1648,7 @@ mod tests { &data.inserted_community, &data.inserted_post, ); - let agg = PostAggregates::read(pool, inserted_post.id) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let agg = PostAggregates::read(pool, inserted_post.id).await?; Ok(PostView { post: Post { @@ -1682,6 +1675,7 @@ mod tests { featured_community: false, featured_local: false, url_content_type: None, + scheduled_publish_time: None, }, my_vote: None, unread_comments: 0, @@ -1700,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, @@ -1723,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, @@ -1735,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, @@ -1770,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?; @@ -1799,8 +1792,8 @@ mod tests { .await?; assert_eq!(2, authenticated_query.len()); - let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await?; - assert!(unauthenticated_post.is_none()); + let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await; + assert!(unauthenticated_post.is_err()); let authenticated_post = PostView::read( pool, @@ -1818,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?; @@ -1850,8 +1843,7 @@ mod tests { Some(&inserted_banned_from_comm_local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; assert!(post_view.banned_from_community); @@ -1862,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?; @@ -1872,11 +1864,193 @@ mod tests { Some(&data.local_user_view.local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; assert!(!post_view.banned_from_community); 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 f5e70fb3e..e59d99608 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -78,7 +78,7 @@ impl PrivateMessageReportView { pub async fn read( pool: &mut DbPool<'_>, report_id: PrivateMessageReportId, - ) -> Result, Error> { + ) -> Result { queries().read(pool, report_id).await } @@ -111,8 +111,7 @@ impl PrivateMessageReportQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::private_message_report_view::PrivateMessageReportQuery; @@ -127,32 +126,31 @@ mod tests { traits::{Crud, Reportable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> LemmyResult<()> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person_1 = PersonInsertForm::test_form(inserted_instance.id, "timmy_mrv"); - let inserted_timmy = Person::create(pool, &new_person_1).await.unwrap(); + let inserted_timmy = Person::create(pool, &new_person_1).await?; let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); - let inserted_jessica = Person::create(pool, &new_person_2).await.unwrap(); + let inserted_jessica = Person::create(pool, &new_person_2).await?; // timmy sends private message to jessica - let pm_form = PrivateMessageInsertForm::builder() - .creator_id(inserted_timmy.id) - .recipient_id(inserted_jessica.id) - .content("something offensive".to_string()) - .build(); - let pm = PrivateMessage::create(pool, &pm_form).await.unwrap(); + let pm_form = PrivateMessageInsertForm::new( + inserted_timmy.id, + inserted_jessica.id, + "something offensive".to_string(), + ); + let pm = PrivateMessage::create(pool, &pm_form).await?; // jessica reports private message let pm_report_form = PrivateMessageReportForm { @@ -161,14 +159,9 @@ mod tests { private_message_id: pm.id, reason: "its offensive".to_string(), }; - let pm_report = PrivateMessageReport::report(pool, &pm_report_form) - .await - .unwrap(); + let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; - let reports = PrivateMessageReportQuery::default() - .list(pool) - .await - .unwrap(); + let reports = PrivateMessageReportQuery::default().list(pool).await?; assert_length!(1, reports); assert!(!reports[0].private_message_report.resolved); assert_eq!(inserted_timmy.name, reports[0].private_message_creator.name); @@ -177,28 +170,27 @@ mod tests { assert_eq!(pm.content, reports[0].private_message.content); let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "admin_mrv"); - let inserted_admin = Person::create(pool, &new_person_3).await.unwrap(); + let inserted_admin = Person::create(pool, &new_person_3).await?; // admin resolves the report (after taking appropriate action) - PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id) - .await - .unwrap(); + PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id).await?; let reports = PrivateMessageReportQuery { unresolved_only: (false), ..Default::default() } .list(pool) - .await - .unwrap(); + .await?; assert_length!(1, reports); assert!(reports[0].private_message_report.resolved); assert!(reports[0].resolver.is_some()); assert_eq!( - inserted_admin.name, - reports[0].resolver.as_ref().unwrap().name + Some(&inserted_admin.name), + reports[0].resolver.as_ref().map(|r| &r.name) ); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 79224d86f..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 { @@ -113,7 +109,7 @@ impl PrivateMessageView { pub async fn read( pool: &mut DbPool<'_>, private_message_id: PrivateMessageId, - ) -> Result, Error> { + ) -> Result { queries().read(pool, private_message_id).await } @@ -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)) @@ -173,8 +165,7 @@ impl PrivateMessageQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView}; @@ -205,57 +196,35 @@ mod tests { async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { let message_content = String::new(); - let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_rav"); - let timmy = Person::create(pool, &timmy_form).await.unwrap(); + let timmy = Person::create(pool, &timmy_form).await?; let sara_form = PersonInsertForm::test_form(instance.id, "sara_rav"); - let sara = Person::create(pool, &sara_form).await.unwrap(); + let sara = Person::create(pool, &sara_form).await?; let jess_form = PersonInsertForm::test_form(instance.id, "jess_rav"); - let jess = Person::create(pool, &jess_form).await.unwrap(); + let jess = Person::create(pool, &jess_form).await?; - let sara_timmy_message_form = PrivateMessageInsertForm::builder() - .creator_id(sara.id) - .recipient_id(timmy.id) - .content(message_content.clone()) - .build(); - PrivateMessage::create(pool, &sara_timmy_message_form) - .await - .unwrap(); + let sara_timmy_message_form = + PrivateMessageInsertForm::new(sara.id, timmy.id, message_content.clone()); + PrivateMessage::create(pool, &sara_timmy_message_form).await?; - let sara_jess_message_form = PrivateMessageInsertForm::builder() - .creator_id(sara.id) - .recipient_id(jess.id) - .content(message_content.clone()) - .build(); - PrivateMessage::create(pool, &sara_jess_message_form) - .await - .unwrap(); + let sara_jess_message_form = + PrivateMessageInsertForm::new(sara.id, jess.id, message_content.clone()); + PrivateMessage::create(pool, &sara_jess_message_form).await?; - let timmy_sara_message_form = PrivateMessageInsertForm::builder() - .creator_id(timmy.id) - .recipient_id(sara.id) - .content(message_content.clone()) - .build(); - PrivateMessage::create(pool, &timmy_sara_message_form) - .await - .unwrap(); + let timmy_sara_message_form = + PrivateMessageInsertForm::new(timmy.id, sara.id, message_content.clone()); + PrivateMessage::create(pool, &timmy_sara_message_form).await?; - let jess_timmy_message_form = PrivateMessageInsertForm::builder() - .creator_id(jess.id) - .recipient_id(timmy.id) - .content(message_content.clone()) - .build(); - PrivateMessage::create(pool, &jess_timmy_message_form) - .await - .unwrap(); + let jess_timmy_message_form = + PrivateMessageInsertForm::new(jess.id, timmy.id, message_content.clone()); + PrivateMessage::create(pool, &jess_timmy_message_form).await?; Ok(Data { instance, @@ -267,14 +236,14 @@ mod tests { async fn cleanup(instance_id: InstanceId, pool: &mut DbPool<'_>) -> LemmyResult<()> { // This also deletes all persons and private messages thanks to sql `on delete cascade` - Instance::delete(pool, instance_id).await.unwrap(); + Instance::delete(pool, instance_id).await?; Ok(()) } #[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, @@ -289,8 +258,7 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(3, &timmy_messages); assert_eq!(timmy_messages[0].creator.id, jess.id); @@ -306,8 +274,7 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(2, &timmy_unread_messages); assert_eq!(timmy_unread_messages[0].creator.id, jess.id); @@ -321,8 +288,7 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(2, &timmy_sara_messages); assert_eq!(timmy_sara_messages[0].creator.id, timmy.id); @@ -336,8 +302,7 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(1, &timmy_sara_unread_messages); assert_eq!(timmy_sara_unread_messages[0].creator.id, sara.id); @@ -349,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, @@ -364,9 +329,7 @@ mod tests { target_id: sara.id, }; - let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form) - .await - .unwrap(); + let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form).await?; let expected_block = PersonBlock { person_id: timmy.id, @@ -381,14 +344,11 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(1, &timmy_messages); - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id) - .await - .unwrap(); + let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; assert_eq!(timmy_unread_messages, 1); cleanup(instance.id, pool).await @@ -397,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, @@ -411,9 +371,7 @@ mod tests { instance_id: sara.instance_id, }; - let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form) - .await - .unwrap(); + let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form).await?; let expected_instance_block = InstanceBlock { person_id: timmy.id, @@ -428,14 +386,11 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(0, &timmy_messages); - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id) - .await - .unwrap(); + let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; assert_eq!(timmy_unread_messages, 0); cleanup(instance.id, pool).await } diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index 54c7f7598..b5821ef26 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -81,17 +81,11 @@ fn queries<'a>() -> Queries< } impl RegistrationApplicationView { - pub async fn read( - pool: &mut DbPool<'_>, - id: RegistrationApplicationId, - ) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, id: RegistrationApplicationId) -> Result { queries().read(pool, ReadBy::Id(id)).await } - pub async fn read_by_person( - pool: &mut DbPool<'_>, - person_id: PersonId, - ) -> Result, Error> { + pub async fn read_by_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { queries().read(pool, ReadBy::Person(person_id)).await } /// Returns the current unread registration_application count @@ -141,8 +135,6 @@ impl RegistrationApplicationQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::registration_application_view::{ @@ -163,38 +155,34 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { - let pool = &build_db_pool_for_tests().await; + async fn test_crud() -> LemmyResult<()> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rav"); - let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap(); + let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?; let timmy_local_user_form = LocalUserInsertForm::test_form_admin(inserted_timmy_person.id); - let _inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]) - .await - .unwrap(); + let _inserted_timmy_local_user = + LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rav"); - let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap(); + let inserted_sara_person = Person::create(pool, &sara_person_form).await?; let sara_local_user_form = LocalUserInsertForm::test_form(inserted_sara_person.id); - let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]) - .await - .unwrap(); + let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]).await?; // Sara creates an application let sara_app_form = RegistrationApplicationInsertForm { @@ -202,24 +190,17 @@ mod tests { answer: "LET ME IIIIINN".to_string(), }; - let sara_app = RegistrationApplication::create(pool, &sara_app_form) - .await - .unwrap(); + let sara_app = RegistrationApplication::create(pool, &sara_app_form).await?; - let read_sara_app_view = RegistrationApplicationView::read(pool, sara_app.id) - .await - .unwrap() - .unwrap(); + let read_sara_app_view = RegistrationApplicationView::read(pool, sara_app.id).await?; let jess_person_form = PersonInsertForm::test_form(inserted_instance.id, "jess_rav"); - let inserted_jess_person = Person::create(pool, &jess_person_form).await.unwrap(); + let inserted_jess_person = Person::create(pool, &jess_person_form).await?; let jess_local_user_form = LocalUserInsertForm::test_form(inserted_jess_person.id); - let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]) - .await - .unwrap(); + let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]).await?; // Sara creates an application let jess_app_form = RegistrationApplicationInsertForm { @@ -227,14 +208,9 @@ mod tests { answer: "LET ME IIIIINN".to_string(), }; - let jess_app = RegistrationApplication::create(pool, &jess_app_form) - .await - .unwrap(); + let jess_app = RegistrationApplication::create(pool, &jess_app_form).await?; - let read_jess_app_view = RegistrationApplicationView::read(pool, jess_app.id) - .await - .unwrap() - .unwrap(); + let read_jess_app_view = RegistrationApplicationView::read(pool, jess_app.id).await?; let mut expected_sara_app_view = RegistrationApplicationView { registration_application: sara_app.clone(), @@ -243,16 +219,15 @@ mod tests { person_id: inserted_sara_local_user.person_id, email: inserted_sara_local_user.email, show_nsfw: inserted_sara_local_user.show_nsfw, - auto_expand: inserted_sara_local_user.auto_expand, blur_nsfw: inserted_sara_local_user.blur_nsfw, theme: inserted_sara_local_user.theme, - default_sort_type: inserted_sara_local_user.default_sort_type, + default_post_sort_type: inserted_sara_local_user.default_post_sort_type, + default_comment_sort_type: inserted_sara_local_user.default_comment_sort_type, default_listing_type: inserted_sara_local_user.default_listing_type, interface_language: inserted_sara_local_user.interface_language, show_avatars: inserted_sara_local_user.show_avatars, send_notifications_to_email: inserted_sara_local_user.send_notifications_to_email, show_bot_accounts: inserted_sara_local_user.show_bot_accounts, - show_scores: inserted_sara_local_user.show_scores, show_read_posts: inserted_sara_local_user.show_read_posts, email_verified: inserted_sara_local_user.email_verified, accepted_application: inserted_sara_local_user.accepted_application, @@ -265,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 { @@ -283,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, @@ -301,8 +276,7 @@ mod tests { ..Default::default() } .list(pool) - .await - .unwrap(); + .await?; assert_eq!( apps, @@ -310,9 +284,7 @@ mod tests { ); // Make sure the counts are correct - let unread_count = RegistrationApplicationView::get_unread_count(pool, false) - .await - .unwrap(); + let unread_count = RegistrationApplicationView::get_unread_count(pool, false).await?; assert_eq!(unread_count, 2); // Approve the application @@ -321,9 +293,7 @@ mod tests { deny_reason: None, }; - RegistrationApplication::update(pool, sara_app.id, &approve_form) - .await - .unwrap(); + RegistrationApplication::update(pool, sara_app.id, &approve_form).await?; // Update the local_user row let approve_local_user_form = LocalUserUpdateForm { @@ -331,14 +301,10 @@ mod tests { ..Default::default() }; - LocalUser::update(pool, inserted_sara_local_user.id, &approve_local_user_form) - .await - .unwrap(); + LocalUser::update(pool, inserted_sara_local_user.id, &approve_local_user_form).await?; - let read_sara_app_view_after_approve = RegistrationApplicationView::read(pool, sara_app.id) - .await - .unwrap() - .unwrap(); + let read_sara_app_view_after_approve = + RegistrationApplicationView::read(pool, sara_app.id).await?; // Make sure the columns changed expected_sara_app_view @@ -362,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, @@ -378,28 +343,23 @@ mod tests { ..Default::default() } .list(pool) - .await - .unwrap(); + .await?; assert_eq!(apps_after_resolve, vec![read_jess_app_view]); // Make sure the counts are correct - let unread_count_after_approve = RegistrationApplicationView::get_unread_count(pool, false) - .await - .unwrap(); + let unread_count_after_approve = + RegistrationApplicationView::get_unread_count(pool, false).await?; assert_eq!(unread_count_after_approve, 1); // Make sure the not undenied_only has all the apps - let all_apps = RegistrationApplicationQuery::default() - .list(pool) - .await - .unwrap(); + let all_apps = RegistrationApplicationQuery::default().list(pool).await?; assert_eq!(all_apps.len(), 2); - Person::delete(pool, inserted_timmy_person.id) - .await - .unwrap(); - Person::delete(pool, inserted_sara_person.id).await.unwrap(); - Person::delete(pool, inserted_jess_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Person::delete(pool, inserted_timmy_person.id).await?; + Person::delete(pool, inserted_sara_person.id).await?; + Person::delete(pool, inserted_jess_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views/src/site_view.rs b/crates/db_views/src/site_view.rs index 8f0722318..ed9aeb498 100644 --- a/crates/db_views/src/site_view.rs +++ b/crates/db_views/src/site_view.rs @@ -1,28 +1,32 @@ use crate::structs::SiteView; -use diesel::{result::Error, ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl}; +use diesel::{ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ schema::{local_site, local_site_rate_limit, site, site_aggregates}, utils::{get_conn, DbPool}, }; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl SiteView { - pub async fn read_local(pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn read_local(pool: &mut DbPool<'_>) -> LemmyResult { let conn = &mut get_conn(pool).await?; - site::table - .inner_join(local_site::table) - .inner_join( - local_site_rate_limit::table.on(local_site::id.eq(local_site_rate_limit::local_site_id)), - ) - .inner_join(site_aggregates::table) - .select(( - site::all_columns, - local_site::all_columns, - local_site_rate_limit::all_columns, - site_aggregates::all_columns, - )) - .first(conn) - .await - .optional() + Ok( + site::table + .inner_join(local_site::table) + .inner_join( + local_site_rate_limit::table.on(local_site::id.eq(local_site_rate_limit::local_site_id)), + ) + .inner_join(site_aggregates::table) + .select(( + site::all_columns, + local_site::all_columns, + local_site_rate_limit::all_columns, + site_aggregates::all_columns, + )) + .first(conn) + .await + .optional()? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?, + ) } } 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 5daa072c3..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) @@ -83,8 +77,6 @@ impl VoteView { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::structs::VoteView; @@ -99,51 +91,47 @@ mod tests { traits::{Bannable, Crud, Likeable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn post_and_comment_vote_views() { - let pool = &build_db_pool_for_tests().await; + async fn post_and_comment_vote_views() -> LemmyResult<()> { + 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 - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_vv"); - let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); + let inserted_timmy = Person::create(pool, &new_person).await?; let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_vv"); - let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); + let inserted_sara = Person::create(pool, &new_person_2).await?; - let new_community = CommunityInsertForm::builder() - .name("test community vv".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community vv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let new_post = PostInsertForm::new( + "A test post vv".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; - let new_post = PostInsertForm::builder() - .name("A test post vv".into()) - .creator_id(inserted_timmy.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment vv".into()) - .creator_id(inserted_timmy.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let comment_form = CommentInsertForm::new( + inserted_timmy.id, + inserted_post.id, + "A test comment vv".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; // Timmy upvotes his own post let timmy_post_vote_form = PostLikeForm { @@ -151,7 +139,7 @@ mod tests { person_id: inserted_timmy.id, score: 1, }; - PostLike::like(pool, &timmy_post_vote_form).await.unwrap(); + PostLike::like(pool, &timmy_post_vote_form).await?; // Sara downvotes timmy's post let sara_post_vote_form = PostLikeForm { @@ -159,7 +147,7 @@ mod tests { person_id: inserted_sara.id, score: -1, }; - PostLike::like(pool, &sara_post_vote_form).await.unwrap(); + PostLike::like(pool, &sara_post_vote_form).await?; let expected_post_vote_views = [ VoteView { @@ -174,32 +162,24 @@ mod tests { }, ]; - let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None) - .await - .unwrap(); + let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None).await?; assert_eq!(read_post_vote_views, expected_post_vote_views); // 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, }; - CommentLike::like(pool, &timmy_comment_vote_form) - .await - .unwrap(); + CommentLike::like(pool, &timmy_comment_vote_form).await?; // 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, }; - CommentLike::like(pool, &sara_comment_vote_form) - .await - .unwrap(); + CommentLike::like(pool, &sara_comment_vote_form).await?; let expected_comment_vote_views = [ VoteView { @@ -214,9 +194,8 @@ mod tests { }, ]; - let read_comment_vote_views = VoteView::list_for_comment(pool, inserted_comment.id, None, None) - .await - .unwrap(); + let read_comment_vote_views = + VoteView::list_for_comment(pool, inserted_comment.id, None, None).await?; assert_eq!(read_comment_vote_views, expected_comment_vote_views); // Ban timmy from that community @@ -225,36 +204,26 @@ mod tests { person_id: inserted_timmy.id, expires: None, }; - CommunityPersonBan::ban(pool, &ban_timmy_form) - .await - .unwrap(); + CommunityPersonBan::ban(pool, &ban_timmy_form).await?; // Make sure creator_banned_from_community is true let read_comment_vote_views_after_ban = - VoteView::list_for_comment(pool, inserted_comment.id, None, None) - .await - .unwrap(); + VoteView::list_for_comment(pool, inserted_comment.id, None, None).await?; - assert!( - read_comment_vote_views_after_ban - .first() - .unwrap() - .creator_banned_from_community - ); + assert!(read_comment_vote_views_after_ban + .first() + .is_some_and(|c| c.creator_banned_from_community)); let read_post_vote_views_after_ban = - VoteView::list_for_post(pool, inserted_post.id, None, None) - .await - .unwrap(); + VoteView::list_for_post(pool, inserted_post.id, None, None).await?; - assert!( - read_post_vote_views_after_ban - .get(1) - .unwrap() - .creator_banned_from_community - ); + assert!(read_post_vote_views_after_ban + .get(1) + .is_some_and(|p| p.creator_banned_from_community)); // Cleanup - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index af139b8b2..18a79826b 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -15,7 +15,13 @@ doctest = false workspace = true [features] -full = ["lemmy_db_schema/full", "diesel", "diesel-async", "ts-rs"] +full = [ + "lemmy_db_schema/full", + "lemmy_utils/full", + "diesel", + "diesel-async", + "ts-rs", +] [dependencies] lemmy_db_schema = { workspace = true } @@ -33,14 +39,11 @@ serde_with = { workspace = true } ts-rs = { workspace = true, optional = true } chrono.workspace = true strum = { workspace = true } +lemmy_utils = { workspace = true, optional = true } [dev-dependencies] 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 b1d95e719..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)?; @@ -242,7 +160,7 @@ impl CommentReplyView { pool: &mut DbPool<'_>, comment_reply_id: CommentReplyId, my_person_id: Option, - ) -> Result, Error> { + ) -> Result { queries().read(pool, (comment_reply_id, my_person_id)).await } @@ -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)) @@ -303,7 +219,6 @@ impl CommentReplyQuery { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{comment_reply_view::CommentReplyQuery, structs::CommentReplyView}; @@ -322,14 +237,14 @@ mod tests { utils::build_db_pool_for_tests, }; use lemmy_db_views::structs::LocalUserView; - use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[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?; @@ -348,29 +263,23 @@ mod tests { let recipient_local_user = LocalUser::create(pool, &LocalUserInsertForm::test_form(recipient_id), vec![]).await?; - let new_community = CommunityInsertForm::builder() - .name("test community lake".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community lake".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); let inserted_community = Community::create(pool, &new_community).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_terry.id) - .community_id(inserted_community.id) - .build(); - + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_terry.id, + inserted_community.id, + ); let inserted_post = Post::create(pool, &new_post).await?; - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_terry.id) - .post_id(inserted_post.id) - .build(); - + let comment_form = + CommentInsertForm::new(inserted_terry.id, inserted_post.id, "A test comment".into()); let inserted_comment = Comment::create(pool, &comment_form, None).await?; let comment_reply_form = CommentReplyInsertForm { @@ -389,9 +298,7 @@ mod tests { published: inserted_reply.published, }; - let read_reply = CommentReply::read(pool, inserted_reply.id) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let read_reply = CommentReply::read(pool, inserted_reply.id).await?; let comment_reply_update_form = CommentReplyUpdateForm { read: Some(false) }; let updated_reply = @@ -446,9 +353,7 @@ mod tests { &recipient_local_user_update_form, ) .await?; - let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id) - .await? - .ok_or(LemmyErrorType::CouldntFindLocalUser)?; + let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id).await?; let unread_replies_after_hide_bots = CommentReplyView::get_unread_replies(pool, &recipient_local_user_view.local_user).await?; diff --git a/crates/db_views_actor/src/community_block_view.rs b/crates/db_views_actor/src/community_block_view.rs deleted file mode 100644 index c7d3d1836..000000000 --- a/crates/db_views_actor/src/community_block_view.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::structs::CommunityBlockView; -use diesel::{result::Error, ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, community_block, person}, - utils::{get_conn, DbPool}, -}; - -impl CommunityBlockView { - pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - community_block::table - .inner_join(person::table) - .inner_join(community::table) - .select((person::all_columns, community::all_columns)) - .filter(community_block::person_id.eq(person_id)) - .filter(community::deleted.eq(false)) - .filter(community::removed.eq(false)) - .order_by(community_block::published) - .load::(conn) - .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 f2a59fd9f..a9ada92e1 100644 --- a/crates/db_views_actor/src/community_moderator_view.rs +++ b/crates/db_views_actor/src/community_moderator_view.rs @@ -1,46 +1,45 @@ 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::{LemmyErrorType, LemmyResult}; impl CommunityModeratorView { - pub async fn is_community_moderator( + pub async fn check_is_community_moderator( pool: &mut DbPool<'_>, find_community_id: CommunityId, find_person_id: PersonId, - ) -> Result { - use lemmy_db_schema::schema::community_moderator::dsl::{ - community_id, - community_moderator, - person_id, - }; + ) -> LemmyResult<()> { 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 + .await? + .then_some(()) + .ok_or(LemmyErrorType::NotAModerator.into()) } pub(crate) async fn is_community_moderator_of_any( pool: &mut DbPool<'_>, find_person_id: PersonId, - ) -> Result { - use lemmy_db_schema::schema::community_moderator::dsl::{community_moderator, person_id}; + ) -> LemmyResult<()> { 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 + .await? + .then_some(()) + .ok_or(LemmyErrorType::NotAModerator.into()) } pub async fn for_community( @@ -48,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 } @@ -64,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(); @@ -90,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 712bb2d3a..224ea8d53 100644 --- a/crates/db_views_actor/src/community_person_ban_view.rs +++ b/crates/db_views_actor/src/community_person_ban_view.rs @@ -1,25 +1,30 @@ use crate::structs::CommunityPersonBanView; -use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl}; +use diesel::{ + dsl::{exists, not}, + select, +}; 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::{LemmyErrorType, LemmyResult}; impl CommunityPersonBanView { - pub async fn get( + pub async fn check( pool: &mut DbPool<'_>, from_person_id: PersonId, from_community_id: CommunityId, - ) -> Result { + ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(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 + .await? + .then_some(()) + .ok_or(LemmyErrorType::PersonIsBannedFromCommunity.into()) } } diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 0e731878a..f42340bdb 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -1,10 +1,9 @@ -use crate::structs::{CommunityModeratorView, CommunityView, PersonView}; +use crate::structs::{CommunityModeratorView, CommunitySortType, CommunityView, PersonView}; use diesel::{ pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, - JoinOnDsl, NullableExpressionMethods, PgTextExpressionMethods, QueryDsl, @@ -13,66 +12,53 @@ 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, + }, + utils::{ + actions, + functions::lower, + fuzzy_search, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, }, - source::{community::CommunityFollower, local_user::LocalUser, site::Site}, - utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, ListingType, - SortType, + PostSortType, }; +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 @@ -102,18 +88,20 @@ fn queries<'a>() -> Queries< }; let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { - use SortType::*; - - // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); + use CommunitySortType::*; let mut query = all_joins(community::table.into_boxed(), options.local_user).select(selection); if let Some(search_term) = options.search_term { let searcher = fuzzy_search(&search_term); - query = query - .filter(community::name.ilike(searcher.clone())) - .or_filter(community::title.ilike(searcher)) + let name_filter = community::name.ilike(searcher.clone()); + let title_filter = community::title.ilike(searcher.clone()); + let description_filter = community::description.ilike(searcher.clone()); + query = if options.title_only.unwrap_or_default() { + query.filter(name_filter.or(title_filter)) + } else { + query.filter(name_filter.or(title_filter.or(description_filter))) + } } // Hide deleted and removed for non-admins or mods @@ -121,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()), ); } @@ -142,11 +130,15 @@ fn queries<'a>() -> Queries< } TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()), TopWeek => query = query.order_by(community_aggregates::users_active_week.desc()), + NameAsc => query = query.order_by(lower(community::name).asc()), + NameDesc => query = query.order_by(lower(community::name).desc()), }; 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, }; @@ -154,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)); } @@ -179,41 +171,71 @@ impl CommunityView { community_id: CommunityId, my_local_user: Option<&'a LocalUser>, is_mod_or_admin: bool, - ) -> Result, Error> { + ) -> Result { queries() .read(pool, (community_id, my_local_user, is_mod_or_admin)) .await } - pub async fn is_mod_or_admin( + pub async fn check_is_mod_or_admin( pool: &mut DbPool<'_>, person_id: PersonId, community_id: CommunityId, - ) -> Result { + ) -> LemmyResult<()> { let is_mod = - CommunityModeratorView::is_community_moderator(pool, community_id, person_id).await?; - if is_mod { - Ok(true) - } else if let Ok(Some(person_view)) = PersonView::read(pool, person_id).await { - Ok(person_view.is_admin) + CommunityModeratorView::check_is_community_moderator(pool, community_id, person_id).await; + if is_mod.is_ok() + || PersonView::read(pool, person_id) + .await + .is_ok_and(|t| t.is_admin) + { + Ok(()) } else { - Ok(false) + Err(LemmyErrorType::NotAModOrAdmin)? } } /// Checks if a person is an admin, or moderator of any community. - pub async fn is_mod_of_any_or_admin( + pub async fn check_is_mod_of_any_or_admin( pool: &mut DbPool<'_>, person_id: PersonId, - ) -> Result { + ) -> LemmyResult<()> { let is_mod_of_any = - CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await?; - if is_mod_of_any { - Ok(true) - } else if let Ok(Some(person_view)) = PersonView::read(pool, person_id).await { - Ok(person_view.is_admin) + CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await; + if is_mod_of_any.is_ok() + || PersonView::read(pool, person_id) + .await + .is_ok_and(|t| t.is_admin) + { + Ok(()) } else { - Ok(false) + Err(LemmyErrorType::NotAModOrAdmin)? + } + } +} + +impl From for CommunitySortType { + fn from(value: PostSortType) -> Self { + match value { + PostSortType::Active => Self::Active, + PostSortType::Hot => Self::Hot, + PostSortType::New => Self::New, + PostSortType::Old => Self::Old, + PostSortType::TopDay => Self::TopDay, + PostSortType::TopWeek => Self::TopWeek, + PostSortType::TopMonth => Self::TopMonth, + PostSortType::TopYear => Self::TopYear, + PostSortType::TopAll => Self::TopAll, + PostSortType::MostComments => Self::MostComments, + PostSortType::NewComments => Self::NewComments, + PostSortType::TopHour => Self::TopHour, + PostSortType::TopSixHour => Self::TopSixHour, + PostSortType::TopTwelveHour => Self::TopTwelveHour, + PostSortType::TopThreeMonths => Self::TopThreeMonths, + PostSortType::TopSixMonths => Self::TopSixMonths, + PostSortType::TopNineMonths => Self::TopNineMonths, + PostSortType::Controversial => Self::Controversial, + PostSortType::Scaled => Self::Scaled, } } } @@ -221,9 +243,10 @@ impl CommunityView { #[derive(Default)] pub struct CommunityQuery<'a> { pub listing_type: Option, - pub sort: Option, + pub sort: Option, pub local_user: Option<&'a LocalUser>, pub search_term: Option, + pub title_only: Option, pub is_mod_or_admin: bool, pub show_nsfw: bool, pub page: Option, @@ -237,59 +260,89 @@ impl<'a> CommunityQuery<'a> { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { - use crate::{community_view::CommunityQuery, structs::CommunityView}; + use crate::{ + community_view::CommunityQuery, + structs::{CommunitySortType, CommunityView}, + }; 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_community: Community, + communities: [Community; 3], site: Site, } - async fn init_data(pool: &mut DbPool<'_>) -> Data { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + 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.unwrap(); + 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 - .unwrap(); + let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; - let new_community = CommunityInsertForm::builder() - .name("test_community_3".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let communities = [ + Community::create( + pool, + &CommunityInsertForm::new( + instance.id, + "test_community_1".to_string(), + "nada1".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + Community::create( + pool, + &CommunityInsertForm::new( + instance.id, + "test_community_2".to_string(), + "nada2".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + Community::create( + pool, + &CommunityInsertForm::new( + instance.id, + "test_community_3".to_string(), + "nada3".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + ]; - let inserted_community = Community::create(pool, &new_community).await.unwrap(); - - let url = Url::parse("http://example.com").unwrap(); + let url = Url::parse("http://example.com")?; let site = Site { id: Default::default(), name: String::new(), @@ -308,76 +361,154 @@ mod tests { content_warning: None, }; - Data { - inserted_instance, + Ok(Data { + instance, local_user, - inserted_community, + communities, site, - } + }) } - async fn cleanup(data: Data, pool: &mut DbPool<'_>) { - Community::delete(pool, data.inserted_community.id) - .await - .unwrap(); - Person::delete(pool, data.local_user.person_id) - .await - .unwrap(); - Instance::delete(pool, data.inserted_instance.id) - .await - .unwrap(); + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + for Community { id, .. } in data.communities { + Community::delete(pool, id).await?; + } + Person::delete(pool, data.local_user.person_id).await?; + Instance::delete(pool, data.instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn local_only_community() { - let pool = &build_db_pool_for_tests().await; + async fn subscribe_state() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let data = init_data(pool).await; + 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(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; Community::update( pool, - data.inserted_community.id, + data.communities[0].id, &CommunityUpdateForm { visibility: Some(CommunityVisibility::LocalOnly), ..Default::default() }, ) - .await - .unwrap(); + .await?; let unauthenticated_query = CommunityQuery { ..Default::default() } .list(&data.site, pool) - .await - .unwrap(); - assert_eq!(0, unauthenticated_query.len()); + .await?; + assert_eq!(data.communities.len() - 1, unauthenticated_query.len()); let authenticated_query = CommunityQuery { local_user: Some(&data.local_user), ..Default::default() } .list(&data.site, pool) - .await - .unwrap(); - assert_eq!(1, authenticated_query.len()); + .await?; + assert_eq!(data.communities.len(), authenticated_query.len()); let unauthenticated_community = - CommunityView::read(pool, data.inserted_community.id, None, false) - .await - .unwrap(); - assert!(unauthenticated_community.is_none()); + CommunityView::read(pool, data.communities[0].id, None, false).await; + assert!(unauthenticated_community.is_err()); - let authenticated_community = CommunityView::read( - pool, - data.inserted_community.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; + cleanup(data, pool).await + } + + #[tokio::test] + #[serial] + async fn community_sort_name() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + let query = CommunityQuery { + sort: Some(CommunitySortType::NameAsc), + ..Default::default() + }; + let communities = query.list(&data.site, pool).await?; + for (i, c) in communities.iter().enumerate().skip(1) { + let prev = communities.get(i - 1).expect("No previous community?"); + assert!(c.community.title.cmp(&prev.community.title).is_ge()); + } + + let query = CommunityQuery { + sort: Some(CommunitySortType::NameDesc), + ..Default::default() + }; + let communities = query.list(&data.site, pool).await?; + for (i, c) in communities.iter().enumerate().skip(1) { + let prev = communities.get(i - 1).expect("No previous community?"); + assert!(c.community.title.cmp(&prev.community.title).is_le()); + } + + cleanup(data, pool).await } } diff --git a/crates/db_views_actor/src/instance_block_view.rs b/crates/db_views_actor/src/instance_block_view.rs deleted file mode 100644 index 05820862a..000000000 --- a/crates/db_views_actor/src/instance_block_view.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::structs::InstanceBlockView; -use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{instance, instance_block, person, site}, - utils::{get_conn, DbPool}, -}; - -impl InstanceBlockView { - pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - instance_block::table - .inner_join(person::table) - .inner_join(instance::table) - .left_join(site::table.on(site::instance_id.eq(instance::id))) - .select(( - person::all_columns, - instance::all_columns, - site::all_columns.nullable(), - )) - .filter(instance_block::person_id.eq(person_id)) - .order_by(instance_block::published) - .load::(conn) - .await - } -} diff --git a/crates/db_views_actor/src/lib.rs b/crates/db_views_actor/src/lib.rs index e9f8e4189..2ec9652e3 100644 --- a/crates/db_views_actor/src/lib.rs +++ b/crates/db_views_actor/src/lib.rs @@ -1,8 +1,6 @@ #[cfg(feature = "full")] pub mod comment_reply_view; #[cfg(feature = "full")] -pub mod community_block_view; -#[cfg(feature = "full")] pub mod community_follower_view; #[cfg(feature = "full")] pub mod community_moderator_view; @@ -11,10 +9,6 @@ pub mod community_person_ban_view; #[cfg(feature = "full")] pub mod community_view; #[cfg(feature = "full")] -pub mod instance_block_view; -#[cfg(feature = "full")] -pub mod person_block_view; -#[cfg(feature = "full")] pub mod person_mention_view; #[cfg(feature = "full")] pub mod person_view; diff --git a/crates/db_views_actor/src/person_block_view.rs b/crates/db_views_actor/src/person_block_view.rs deleted file mode 100644 index 5f028acd8..000000000 --- a/crates/db_views_actor/src/person_block_view.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::structs::PersonBlockView; -use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{person, person_block}, - utils::{get_conn, DbPool}, -}; - -impl PersonBlockView { - pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { - 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))) - .inner_join( - target_person_alias.on(person_block::target_id.eq(target_person_alias.field(person::id))), - ) - .select(( - person::all_columns, - target_person_alias.fields(person::all_columns), - )) - .filter(person_block::person_id.eq(person_id)) - .filter(target_person_alias.field(person::deleted).eq(false)) - .order_by(person_block::published) - .load::(conn) - .await - } -} diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index d6fd7363d..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)?; @@ -241,7 +160,7 @@ impl PersonMentionView { pool: &mut DbPool<'_>, person_mention_id: PersonMentionId, my_person_id: Option, - ) -> Result, Error> { + ) -> Result { queries() .read(pool, (person_mention_id, my_person_id)) .await @@ -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)) @@ -303,7 +220,6 @@ impl PersonMentionQuery { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{person_mention_view::PersonMentionQuery, structs::PersonMentionView}; @@ -322,14 +238,14 @@ mod tests { utils::build_db_pool_for_tests, }; use lemmy_db_views::structs::LocalUserView; - use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[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?; @@ -346,29 +262,26 @@ mod tests { let recipient_local_user = LocalUser::create(pool, &LocalUserInsertForm::test_form(recipient_id), vec![]).await?; - let new_community = CommunityInsertForm::builder() - .name("test community lake".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community lake".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); let inserted_community = Community::create(pool, &new_community).await?; - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); - + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); let inserted_post = Post::create(pool, &new_post).await?; - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); let inserted_comment = Comment::create(pool, &comment_form, None).await?; let person_mention_form = PersonMentionInsertForm { @@ -387,9 +300,7 @@ mod tests { published: inserted_mention.published, }; - let read_mention = PersonMention::read(pool, inserted_mention.id) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let read_mention = PersonMention::read(pool, inserted_mention.id).await?; let person_mention_update_form = PersonMentionUpdateForm { read: Some(false) }; let updated_mention = @@ -445,9 +356,7 @@ mod tests { &recipient_local_user_update_form, ) .await?; - let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id) - .await? - .ok_or(LemmyErrorType::CouldntFindLocalUser)?; + let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id).await?; let unread_mentions_after_hide_bots = PersonMentionView::get_unread_mentions(pool, &recipient_local_user_view.local_user).await?; diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 7a2edfb44..39d1ac27c 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -24,7 +24,7 @@ use lemmy_db_schema::{ ReadFn, }, ListingType, - SortType, + PostSortType, }; use serde::{Deserialize, Serialize}; use strum::{Display, EnumString}; @@ -46,12 +46,13 @@ enum PersonSortType { PostCount, } -fn post_to_person_sort_type(sort: SortType) -> PersonSortType { +fn post_to_person_sort_type(sort: PostSortType) -> PersonSortType { + use PostSortType::*; match sort { - SortType::Active | SortType::Hot | SortType::Controversial => PersonSortType::CommentScore, - SortType::New | SortType::NewComments => PersonSortType::New, - SortType::MostComments => PersonSortType::MostComments, - SortType::Old => PersonSortType::Old, + Active | Hot | Controversial => PersonSortType::CommentScore, + New | NewComments => PersonSortType::New, + MostComments => PersonSortType::MostComments, + Old => PersonSortType::Old, _ => PersonSortType::CommentScore, } } @@ -134,7 +135,7 @@ fn queries<'a>( } impl PersonView { - pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { queries().read(pool, person_id).await } @@ -149,7 +150,7 @@ impl PersonView { #[derive(Default)] pub struct PersonQuery { - pub sort: Option, + pub sort: Option, pub search_term: Option, pub listing_type: Option, pub page: Option, @@ -163,7 +164,7 @@ impl PersonQuery { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use super::*; @@ -177,7 +178,7 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; - use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; @@ -228,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?; @@ -242,11 +243,11 @@ mod tests { ) .await?; - let read = PersonView::read(pool, data.alice.id).await?; - assert!(read.is_none()); + let read = PersonView::read(pool, data.alice.id).await; + assert!(read.is_err()); let list = PersonQuery { - sort: Some(SortType::New), + sort: Some(PostSortType::New), ..Default::default() } .list(pool) @@ -260,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?; @@ -284,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?; @@ -302,16 +303,10 @@ mod tests { assert_length!(1, list); assert_eq!(list[0].person.id, data.alice.id); - let is_admin = PersonView::read(pool, data.alice.id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)? - .is_admin; + let is_admin = PersonView::read(pool, data.alice.id).await?.is_admin; assert!(is_admin); - let is_admin = PersonView::read(pool, data.bob.id) - .await? - .ok_or(LemmyErrorType::CouldntFindPerson)? - .is_admin; + let is_admin = PersonView::read(pool, data.bob.id).await?.is_admin; assert!(!is_admin); cleanup(data, pool).await @@ -320,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 2356d2be4..6b609a753 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -6,11 +6,9 @@ use lemmy_db_schema::{ comment::Comment, comment_reply::CommentReply, community::Community, - instance::Instance, person::Person, person_mention::PersonMention, post::Post, - site::Site, }, SubscribedType, }; @@ -19,28 +17,6 @@ use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -#[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))] -/// A community block. -pub struct CommunityBlockView { - pub person: Person, - pub community: Community, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// An instance block by a user. -pub struct InstanceBlockView { - pub person: Person, - pub instance: Instance, - pub site: Option, -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] @@ -83,14 +59,33 @@ pub struct CommunityView { pub banned_from_community: bool, } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// The community sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// A person block. -pub struct PersonBlockView { - pub person: Person, - pub target: Person, +pub enum CommunitySortType { + #[default] + Active, + Hot, + New, + Old, + TopDay, + TopWeek, + TopMonth, + TopYear, + TopAll, + MostComments, + NewComments, + TopHour, + TopSixHour, + TopTwelveHour, + TopThreeMonths, + TopSixMonths, + TopNineMonths, + Controversial, + Scaled, + NameAsc, + NameDesc, } #[skip_serializing_none] @@ -114,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, } @@ -138,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, } @@ -151,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/Cargo.toml b/crates/federate/Cargo.toml index 6b76dbf97..5d7454276 100644 --- a/crates/federate/Cargo.toml +++ b/crates/federate/Cargo.toml @@ -32,7 +32,7 @@ serde_json.workspace = true tokio = { workspace = true, features = ["full"] } tracing.workspace = true moka.workspace = true -tokio-util = "0.7.11" +tokio-util = "0.7.12" async-trait.workspace = true [dev-dependencies] diff --git a/crates/federate/src/inboxes.rs b/crates/federate/src/inboxes.rs index cda4da39b..1649e019f 100644 --- a/crates/federate/src/inboxes.rs +++ b/crates/federate/src/inboxes.rs @@ -222,14 +222,14 @@ impl CommunityInboxCollector { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use super::*; use lemmy_db_schema::{ newtypes::{ActivityId, CommunityId, InstanceId, SiteId}, source::activity::{ActorType, SentActivity}, }; + use lemmy_utils::error::LemmyResult; use mockall::{mock, predicate::*}; use serde_json::json; mock! { @@ -253,13 +253,11 @@ mod tests { } #[tokio::test] - async fn test_get_inbox_urls_empty() { + async fn test_get_inbox_urls_empty() -> LemmyResult<()> { let mut collector = setup_collector(); let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -270,14 +268,16 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert!(result.is_empty()); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_send_all_instances() { + async fn test_get_inbox_urls_send_all_instances() -> LemmyResult<()> { let mut collector = setup_collector(); - let site_inbox = Url::parse("https://example.com/inbox").unwrap(); + let site_inbox = Url::parse("https://example.com/inbox")?; let site = Site { id: SiteId(1), name: "Test Site".to_string(), @@ -287,7 +287,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site").unwrap().into(), + actor_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, @@ -303,9 +303,7 @@ mod tests { let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -316,13 +314,15 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 1); assert_eq!(result[0], site_inbox); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_community_followers() { + async fn test_get_inbox_urls_community_followers() -> LemmyResult<()> { let mut collector = setup_collector(); let community_id = CommunityId(1); let url1 = "https://follower1.example.com/inbox"; @@ -333,18 +333,22 @@ mod tests { .expect_get_instance_followed_community_inboxes() .return_once(move |_, _| { Ok(vec![ - (community_id, Url::parse(url1).unwrap().into()), - (community_id, Url::parse(url2).unwrap().into()), + ( + community_id, + Url::parse(url1).map_err(|_| diesel::NotFound)?.into(), + ), + ( + community_id, + Url::parse(url2).map_err(|_| diesel::NotFound)?.into(), + ), ]) }); - collector.update_communities().await.unwrap(); + collector.update_communities().await?; let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -355,24 +359,24 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 2); - assert!(result.contains(&Url::parse(url1).unwrap())); - assert!(result.contains(&Url::parse(url2).unwrap())); + assert!(result.contains(&Url::parse(url1)?)); + assert!(result.contains(&Url::parse(url2)?)); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_send_inboxes() { + async fn test_get_inbox_urls_send_inboxes() -> LemmyResult<()> { let mut collector = setup_collector(); collector.domain = "example.com".to_string(); - let inbox_user_1 = Url::parse("https://example.com/user1/inbox").unwrap(); - let inbox_user_2 = Url::parse("https://example.com/user2/inbox").unwrap(); - let other_domain_inbox = Url::parse("https://other-domain.com/user3/inbox").unwrap(); + let inbox_user_1 = Url::parse("https://example.com/user1/inbox")?; + let inbox_user_2 = Url::parse("https://example.com/user2/inbox")?; + let other_domain_inbox = Url::parse("https://other-domain.com/user3/inbox")?; let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -387,20 +391,22 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 2); assert!(result.contains(&inbox_user_1)); assert!(result.contains(&inbox_user_2)); assert!(!result.contains(&other_domain_inbox)); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_combined() { + async fn test_get_inbox_urls_combined() -> LemmyResult<()> { let mut collector = setup_collector(); collector.domain = "example.com".to_string(); let community_id = CommunityId(1); - let site_inbox = Url::parse("https://example.com/site_inbox").unwrap(); + let site_inbox = Url::parse("https://example.com/site_inbox")?; let site = Site { id: SiteId(1), name: "Test Site".to_string(), @@ -410,7 +416,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site").unwrap().into(), + actor_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, @@ -431,18 +437,18 @@ mod tests { .return_once(move |_, _| { Ok(vec![( community_id, - Url::parse(subdomain_inbox).unwrap().into(), + Url::parse(subdomain_inbox) + .map_err(|_| diesel::NotFound)? + .into(), )]) }); - collector.update_communities().await.unwrap(); - let user1_inbox = Url::parse("https://example.com/user1/inbox").unwrap(); - let user2_inbox = Url::parse("https://other-domain.com/user2/inbox").unwrap(); + collector.update_communities().await?; + let user1_inbox = Url::parse("https://example.com/user1/inbox")?; + let user2_inbox = Url::parse("https://other-domain.com/user2/inbox")?; let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -456,27 +462,29 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 3); assert!(result.contains(&site_inbox)); - assert!(result.contains(&Url::parse(subdomain_inbox).unwrap())); + assert!(result.contains(&Url::parse(subdomain_inbox)?)); assert!(result.contains(&user1_inbox)); assert!(!result.contains(&user2_inbox)); + + Ok(()) } #[tokio::test] - async fn test_update_communities() { + async fn test_update_communities() -> LemmyResult<()> { let mut collector = setup_collector(); let community_id1 = CommunityId(1); let community_id2 = CommunityId(2); let community_id3 = CommunityId(3); let user1_inbox_str = "https://follower1.example.com/inbox"; - let user1_inbox = Url::parse(user1_inbox_str).unwrap(); + let user1_inbox = Url::parse(user1_inbox_str)?; let user2_inbox_str = "https://follower2.example.com/inbox"; - let user2_inbox = Url::parse(user2_inbox_str).unwrap(); + let user2_inbox = Url::parse(user2_inbox_str)?; let user3_inbox_str = "https://follower3.example.com/inbox"; - let user3_inbox = Url::parse(user3_inbox_str).unwrap(); + let user3_inbox = Url::parse(user3_inbox_str)?; collector .data_source @@ -485,42 +493,57 @@ mod tests { .returning(move |_, last_fetch| { if last_fetch == Utc.timestamp_nanos(0) { Ok(vec![ - (community_id1, Url::parse(user1_inbox_str).unwrap().into()), - (community_id2, Url::parse(user2_inbox_str).unwrap().into()), + ( + community_id1, + Url::parse(user1_inbox_str) + .map_err(|_| diesel::NotFound)? + .into(), + ), + ( + community_id2, + Url::parse(user2_inbox_str) + .map_err(|_| diesel::NotFound)? + .into(), + ), ]) } else { Ok(vec![( community_id3, - Url::parse(user3_inbox_str).unwrap().into(), + Url::parse(user3_inbox_str) + .map_err(|_| diesel::NotFound)? + .into(), )]) } }); // First update - collector.update_communities().await.unwrap(); + collector.update_communities().await?; assert_eq!(collector.followed_communities.len(), 2); assert!(collector.followed_communities[&community_id1].contains(&user1_inbox)); assert!(collector.followed_communities[&community_id2].contains(&user2_inbox)); // Simulate time passing - collector.last_full_communities_fetch = Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap(); + collector.last_full_communities_fetch = + Utc::now() - chrono::TimeDelta::try_minutes(3).expect("TimeDelta out of bounds"); collector.last_incremental_communities_fetch = - Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap(); + Utc::now() - chrono::TimeDelta::try_minutes(3).expect("TimeDelta out of bounds"); // Second update (incremental) - collector.update_communities().await.unwrap(); + collector.update_communities().await?; assert_eq!(collector.followed_communities.len(), 3); assert!(collector.followed_communities[&community_id1].contains(&user1_inbox)); assert!(collector.followed_communities[&community_id3].contains(&user3_inbox)); assert!(collector.followed_communities[&community_id2].contains(&user2_inbox)); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_no_duplicates() { + async fn test_get_inbox_urls_no_duplicates() -> LemmyResult<()> { let mut collector = setup_collector(); collector.domain = "example.com".to_string(); let community_id = CommunityId(1); - let site_inbox = Url::parse("https://example.com/site_inbox").unwrap(); + let site_inbox = Url::parse("https://example.com/site_inbox")?; let site_inbox_clone = site_inbox.clone(); let site = Site { id: SiteId(1), @@ -531,7 +554,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site").unwrap().into(), + actor_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, @@ -550,13 +573,11 @@ mod tests { .expect_get_instance_followed_community_inboxes() .return_once(move |_, _| Ok(vec![(community_id, site_inbox_clone.into())])); - collector.update_communities().await.unwrap(); + collector.update_communities().await?; let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -567,8 +588,10 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 1); - assert!(result.contains(&Url::parse("https://example.com/site_inbox").unwrap())); + assert!(result.contains(&Url::parse("https://example.com/site_inbox")?)); + + Ok(()) } } diff --git a/crates/federate/src/lib.rs b/crates/federate/src/lib.rs index 66c0a2872..983749de3 100644 --- a/crates/federate/src/lib.rs +++ b/crates/federate/src/lib.rs @@ -192,8 +192,8 @@ impl SendManager { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::unwrap_used)] +#[expect(clippy::indexing_slicing)] mod test { use super::*; @@ -354,10 +354,10 @@ mod test { let mut data = TestData::init(1, 1).await?; let instance = &data.instances[0]; - let form = InstanceForm::builder() - .domain(instance.domain.clone()) - .updated(DateTime::from_timestamp(0, 0)) - .build(); + let form = InstanceForm { + updated: DateTime::from_timestamp(0, 0), + ..InstanceForm::new(instance.domain.clone()) + }; Instance::update(&mut data.context.pool(), instance.id, form).await?; data.run().await?; diff --git a/crates/federate/src/util.rs b/crates/federate/src/util.rs index e10a01c30..9473aafa3 100644 --- a/crates/federate/src/util.rs +++ b/crates/federate/src/util.rs @@ -183,8 +183,8 @@ pub(crate) async fn get_activity_cached( .try_get_with(activity_id, async { let row = SentActivity::read(pool, activity_id) .await - .context("could not read activity")?; - let Some(mut row) = row else { + .context("could not read activity"); + let Ok(mut row) = row else { return anyhow::Result::<_, anyhow::Error>::Ok(None); }; // swap to avoid cloning diff --git a/crates/federate/src/worker.rs b/crates/federate/src/worker.rs index f6e70d846..b0254ba0b 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -290,10 +290,10 @@ impl InstanceWorker { if updated.add(Days::new(1)) < Utc::now() { self.instance.updated = Some(Utc::now()); - let form = InstanceForm::builder() - .domain(self.instance.domain.clone()) - .updated(Some(naive_now())) - .build(); + let form = InstanceForm { + updated: Some(naive_now()), + ..InstanceForm::new(self.instance.domain.clone()) + }; Instance::update(&mut self.pool(), self.instance.id, form).await?; } Ok(()) @@ -439,8 +439,8 @@ impl InstanceWorker { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::unwrap_used)] +#[expect(clippy::indexing_slicing)] mod test { use super::*; @@ -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::{ @@ -459,7 +459,6 @@ mod test { traits::Crud, }; use lemmy_utils::error::LemmyResult; - use reqwest::StatusCode; use serde_json::{json, Value}; use serial_test::serial; use test_context::{test_context, AsyncTestContext}; @@ -492,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?; @@ -659,10 +657,7 @@ mod test { #[tokio::test] #[serial] async fn test_update_instance(data: &mut Data) -> LemmyResult<()> { - let form = InstanceForm::builder() - .domain(data.instance.domain.clone()) - .updated(None) - .build(); + let form = InstanceForm::new(data.instance.domain.clone()); Instance::update(&mut data.context.pool(), data.instance.id, form).await?; send_activity(data.person.actor_id.clone(), &data.context, true).await?; @@ -688,7 +683,7 @@ mod test { |inbox_sender: actix_web::web::Data>, body: String| async move { tracing::debug!("received activity: {:?}", body); inbox_sender.send(body.clone()).unwrap(); - HttpResponse::new(StatusCode::OK) + HttpResponse::new(actix_web::http::StatusCode::OK) }, ), ) diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index a614ba42d..4a8c53dea 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -32,5 +32,5 @@ serde = { workspace = true } url = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } -urlencoding = { workspace = true } -rss = "2.0.8" +http.workspace = true +rss = "2.0.9" diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index f7e7d4059..00518032d 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -9,7 +9,7 @@ use lemmy_db_schema::{ CommentSortType, CommunityVisibility, ListingType, - SortType, + PostSortType, }; use lemmy_db_views::{ post_view::PostQuery, @@ -27,6 +27,7 @@ use lemmy_utils::{ }; use rss::{ extension::{dublincore::DublinCoreExtension, ExtensionBuilder, ExtensionMap}, + Category, Channel, EnclosureBuilder, Guid, @@ -45,12 +46,12 @@ struct Params { } impl Params { - fn sort_type(&self) -> Result { + fn sort_type(&self) -> Result { let sort_query = self .sort .clone() - .unwrap_or_else(|| SortType::Hot.to_string()); - SortType::from_str(&sort_query).map_err(ErrorBadRequest) + .unwrap_or_else(|| PostSortType::Hot.to_string()); + PostSortType::from_str(&sort_query).map_err(ErrorBadRequest) } fn get_limit(&self) -> i64 { self.limit.unwrap_or(RSS_FETCH_LIMIT) @@ -147,13 +148,11 @@ async fn get_local_feed( async fn get_feed_data( context: &LemmyContext, listing_type: ListingType, - sort_type: SortType, + sort_type: PostSortType, limit: i64, page: i64, ) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&None, &site_view.local_site)?; @@ -253,17 +252,15 @@ async fn get_feed( #[tracing::instrument(skip_all)] async fn get_feed_user( context: &LemmyContext, - sort_type: &SortType, + sort_type: &PostSortType, limit: &i64, page: &i64, user_name: &str, ) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let person = Person::read_from_name(&mut context.pool(), user_name, false) .await? - .ok_or(LemmyErrorType::CouldntFindPerson)?; + .ok_or(LemmyErrorType::NotFound)?; check_private_instance(&None, &site_view.local_site)?; @@ -293,19 +290,17 @@ async fn get_feed_user( #[tracing::instrument(skip_all)] async fn get_feed_community( context: &LemmyContext, - sort_type: &SortType, + sort_type: &PostSortType, limit: &i64, page: &i64, community_name: &str, ) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let community = Community::read_from_name(&mut context.pool(), community_name, false) .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + .ok_or(LemmyErrorType::NotFound)?; if community.visibility != CommunityVisibility::Public { - return Err(LemmyErrorType::CouldntFindCommunity.into()); + return Err(LemmyErrorType::NotFound.into()); } check_private_instance(&None, &site_view.local_site)?; @@ -340,14 +335,12 @@ async fn get_feed_community( #[tracing::instrument(skip_all)] async fn get_feed_front( context: &LemmyContext, - sort_type: &SortType, + sort_type: &PostSortType, limit: &i64, page: &i64, jwt: &str, ) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let local_user = local_user_view_from_jwt(jwt, context).await?; check_private_instance(&Some(local_user.clone()), &site_view.local_site)?; @@ -382,9 +375,7 @@ async fn get_feed_front( #[tracing::instrument(skip_all)] async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()) - .await? - .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let site_view = SiteView::read_local(&mut context.pool()).await?; let local_user = local_user_view_from_jwt(jwt, context).await?; let person_id = local_user.local_user.person_id; let show_bot_accounts = local_user.local_user.show_bot_accounts; @@ -569,6 +560,10 @@ fn create_post_items(posts: Vec, protocol_and_hostname: &str) -> Lemmy BTreeMap::from([("content".to_string(), vec![thumbnail_ext.build()])]), ); } + let category = Category { + name: p.community.title, + domain: Some(p.community.actor_id.to_string()), + }; let i = Item { title: Some(sanitize_html(sanitize_xml(p.post.name).as_str())), @@ -580,6 +575,7 @@ fn create_post_items(posts: Vec, protocol_and_hostname: &str) -> Lemmy link: Some(post_url.clone()), extensions, enclosure: enclosure_opt, + categories: vec![category], ..Default::default() }; diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 10ffb57de..a0f804b6b 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -2,14 +2,15 @@ use actix_web::{ body::BodyStream, http::{ header::{HeaderName, ACCEPT_ENCODING, HOST}, + Method, StatusCode, }, - web, - web::Query, + web::{self, Query}, HttpRequest, HttpResponse, }; use futures::stream::{Stream, StreamExt}; +use http::HeaderValue; use lemmy_api_common::{context::LemmyContext, request::PictrsResponse}; use lemmy_db_schema::source::{ images::{LocalImage, LocalImageForm, RemoteImage}, @@ -22,7 +23,6 @@ use reqwest_middleware::{ClientWithMiddleware, RequestBuilder}; use serde::Deserialize; use std::time::Duration; use url::Url; -use urlencoding::decode; pub fn config( cfg: &mut web::ServiceConfig, @@ -110,7 +110,7 @@ fn adapt_request( const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST]; let client_request = client - .request(request.method().clone(), url) + .request(convert_method(request.method()), url) .timeout(REQWEST_TIMEOUT); request @@ -120,7 +120,8 @@ fn adapt_request( if INVALID_HEADERS.contains(key) { client_req } else { - client_req.header(key, value) + // TODO: remove as_str and as_bytes conversions after actix-web upgrades to http 1.0 + client_req.header(key.as_str(), value.as_bytes()) } }) } @@ -167,7 +168,7 @@ async fn upload( } } - Ok(HttpResponse::build(status).json(images)) + Ok(HttpResponse::build(convert_status(status)).json(images)) } async fn full_res( @@ -210,14 +211,14 @@ async fn image( let res = client_req.send().await?; - if res.status() == StatusCode::NOT_FOUND { + if res.status() == http::StatusCode::NOT_FOUND { return Ok(HttpResponse::NotFound().finish()); } - let mut client_res = HttpResponse::build(res.status()); + let mut client_res = HttpResponse::build(StatusCode::from_u16(res.status().as_u16())?); for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") { - client_res.insert_header((name.clone(), value.clone())); + client_res.insert_header(convert_header(name, value)); } Ok(client_res.body(BodyStream::new(res.bytes_stream()))) @@ -246,7 +247,7 @@ async fn delete( LocalImage::delete_by_alias(&mut context.pool(), &file).await?; - Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream()))) + Ok(HttpResponse::build(convert_status(res.status())).body(BodyStream::new(res.bytes_stream()))) } pub async fn image_proxy( @@ -255,7 +256,7 @@ pub async fn image_proxy( client: web::Data, context: web::Data, ) -> LemmyResult { - let url = Url::parse(&decode(¶ms.url)?)?; + let url = Url::parse(¶ms.url)?; // Check that url corresponds to a federated image so that this can't be abused as a proxy // for arbitrary purposes. @@ -309,3 +310,14 @@ where std::pin::Pin::new(&mut self.rx).poll_recv(cx) } } + +// TODO: remove these conversions after actix-web upgrades to http 1.0 +fn convert_status(status: http::StatusCode) -> StatusCode { + StatusCode::from_u16(status.as_u16()).expect("status can be converted") +} +fn convert_method(method: &Method) -> http::Method { + http::Method::from_bytes(method.as_str().as_bytes()).expect("method can be converted") +} +fn convert_header<'a>(name: &'a http::HeaderName, value: &'a HeaderValue) -> (&'a str, &'a [u8]) { + (name.as_str(), value.as_bytes()) +} diff --git a/crates/routes/src/lib.rs b/crates/routes/src/lib.rs index 4f8d60246..a88225622 100644 --- a/crates/routes/src/lib.rs +++ b/crates/routes/src/lib.rs @@ -1,6 +1,6 @@ use lemmy_api_common::{claims::Claims, context::LemmyContext, utils::check_user_valid}; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::LemmyResult; pub mod feeds; pub mod images; @@ -10,9 +10,7 @@ pub mod webfinger; #[tracing::instrument(skip_all)] async fn local_user_view_from_jwt(jwt: &str, context: &LemmyContext) -> LemmyResult { let local_user_id = Claims::validate(jwt, context).await?; - let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id) - .await? - .ok_or(LemmyErrorType::CouldntFindLocalUser)?; + let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; check_user_valid(&local_user_view.person)?; Ok(local_user_view) diff --git a/crates/routes/src/nodeinfo.rs b/crates/routes/src/nodeinfo.rs index bcb835309..e5b183a0b 100644 --- a/crates/routes/src/nodeinfo.rs +++ b/crates/routes/src/nodeinfo.rs @@ -1,11 +1,10 @@ -use actix_web::{error::ErrorBadRequest, web, Error, HttpResponse, Result}; -use anyhow::anyhow; +use actix_web::{web, Error, HttpResponse, Result}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::RegistrationMode; use lemmy_db_views::structs::SiteView; use lemmy_utils::{ cache_header::{cache_1hour, cache_3days}, - error::{LemmyError, LemmyResult}, + error::LemmyResult, VERSION, }; use serde::{Deserialize, Serialize}; @@ -44,10 +43,7 @@ async fn node_info_well_known(context: web::Data) -> LemmyResult) -> Result { - let site_view = SiteView::read_local(&mut context.pool()) - .await - .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))? - .ok_or(ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?; + let site_view = SiteView::read_local(&mut context.pool()).await?; // Since there are 3 registration options, // we need to set open_registrations as true if RegistrationMode is not Closed. diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs index f2a67c0fc..c5b7024cd 100644 --- a/crates/routes/src/webfinger.rs +++ b/crates/routes/src/webfinger.rs @@ -84,7 +84,7 @@ async fn get_webfinger_response( Ok( HttpResponse::Ok() - .content_type(&WEBFINGER_CONTENT_TYPE) + .content_type(WEBFINGER_CONTENT_TYPE.as_bytes()) .json(json), ) } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index e94fce9d6..c22f863c1 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -32,7 +32,6 @@ full = [ "dep:actix-web", "dep:serde_json", "dep:anyhow", - "dep:tracing-error", "dep:http", "dep:deser-hjson", "dep:regex", @@ -50,10 +49,12 @@ full = [ "dep:markdown-it", ] +[package.metadata.cargo-shear] +ignored = ["http"] + [dependencies] regex = { workspace = true, optional = true } tracing = { workspace = true, optional = true } -tracing-error = { workspace = true, optional = true } itertools = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true, optional = true } @@ -73,7 +74,7 @@ urlencoding = { workspace = true, optional = true } html2text = { version = "0.12.5", optional = true } deser-hjson = { version = "2.2.4", optional = true } smart-default = { version = "0.7.1", optional = true } -lettre = { version = "0.11.7", default-features = false, features = [ +lettre = { version = "0.11.8", default-features = false, features = [ "builder", "tokio1", "tokio1-rustls-tls", @@ -83,9 +84,13 @@ markdown-it = { version = "0.6.1", optional = true } 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 860dad6fd..d52df2f72 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -1,6 +1,6 @@ use cfg_if::cfg_if; use serde::{Deserialize, Serialize}; -use std::fmt::Debug; +use std::{backtrace::Backtrace, fmt::Debug}; use strum::{Display, EnumIter}; #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)] @@ -23,42 +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, - CouldntFindCommunity, - CouldntFindPerson, - CouldntFindComment, - CouldntFindCommentReport, - CouldntFindPostReport, - CouldntFindPrivateMessageReport, - CouldntFindLocalUser, - CouldntFindPersonMention, - CouldntFindRegistrationApplication, - CouldntFindCommentReply, - CouldntFindPrivateMessage, - CouldntFindActivity, PersonIsBlocked, CommunityIsBlocked, InstanceIsBlocked, - DownvotesAreDisabled, InstanceIsPrivate, /// Password must be between 10 and 60 characters InvalidPassword, @@ -73,34 +55,21 @@ pub enum LemmyErrorType { OnlyAdminsCanCreateCommunities, CommunityAlreadyExists, LanguageNotAllowed, - OnlyModsCanPostInCommunity, CouldntUpdatePost, NoPostEditAllowed, - CouldntFindPost, EditPrivateMessageNotAllowed, SiteAlreadyExists, ApplicationQuestionRequired, InvalidDefaultPostListingType, RegistrationClosed, RegistrationApplicationAnswerRequired, + RegistrationUsernameRequired, EmailAlreadyExists, - FederationForbiddenByStrictAllowList, + UsernameAlreadyExists, PersonIsBannedFromCommunity, - ObjectIsNotPublic, - InvalidCommunity, - CannotCreatePostOrCommentInDeletedOrRemovedCommunity, - CannotReceivePage, - NewPostCannotBeLocked, - OnlyLocalAdminCanRemoveCommunity, - OnlyLocalAdminCanRestoreCommunity, NoIdGiven, IncorrectLogin, - InvalidQuery, ObjectNotLocal, - PostIsLocked, - PersonIsBannedFromSite(String), - InvalidVoteValue, - PageDoesNotSpecifyCreator, NoEmailSetup, LocalSiteNotSetup, EmailSmtpServerNeedsAPort, @@ -130,7 +99,6 @@ pub enum LemmyErrorType { CouldntUpdateCommunityHiddenStatus, PersonBlockAlreadyExists, UserAlreadyExists, - TokenNotFound, CouldntLikePost, CouldntSavePost, CouldntMarkPostAsRead, @@ -138,7 +106,6 @@ pub enum LemmyErrorType { CouldntUpdateCommunity, CouldntUpdateReplies, CouldntUpdatePersonMentions, - PostTitleTooLong, CouldntCreatePost, CouldntCreatePrivateMessage, CouldntUpdatePrivate, @@ -152,12 +119,10 @@ pub enum LemmyErrorType { InvalidUrl, EmailSendFailed, Slurs, - CouldntFindObject, - RegistrationDenied(Option), - FederationDisabled, - DomainBlocked(String), - DomainNotInAllowList(String), - FederationDisabledByStrictAllowList, + RegistrationDenied { + #[cfg_attr(feature = "full", ts(optional))] + reason: Option, + }, SiteNameRequired, SiteNameLengthOverflow, PermissiveRegex, @@ -171,29 +136,67 @@ 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, + Unknown(String), + UrlLengthOverflow, + OauthAuthorizationInvalid, + OauthLoginFailed, + OauthRegistrationClosed, + CouldntDeleteOauthProvider, + NotFound, + CommunityHasNoFollowers, + PostScheduleTimeMustBeInFuture, + TooManyScheduledPosts, + 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, - Unknown(String), CantDeleteSite, - UrlLengthOverflow, + ObjectIsNotPublic, + ObjectIsNotPrivate, } cfg_if! { if #[cfg(feature = "full")] { - use tracing_error::SpanTrace; use std::fmt; pub type LemmyResult = Result; pub struct LemmyError { pub error_type: LemmyErrorType, pub inner: anyhow::Error, - pub context: SpanTrace, + pub context: Backtrace, } /// Maximum number of items in an array passed as API parameter. See [[LemmyErrorType::TooManyItems]] @@ -205,10 +208,14 @@ cfg_if! { { fn from(t: T) -> Self { let cause = t.into(); + let error_type = match cause.downcast_ref::() { + Some(&diesel::NotFound) => LemmyErrorType::NotFound, + _ => LemmyErrorType::Unknown(format!("{}", &cause)) + }; LemmyError { - error_type: LemmyErrorType::Unknown(format!("{}", &cause)), + error_type, inner: cause, - context: SpanTrace::capture(), + context: Backtrace::capture(), } } } @@ -232,13 +239,13 @@ cfg_if! { } impl actix_web::error::ResponseError for LemmyError { - fn status_code(&self) -> http::StatusCode { + fn status_code(&self) -> actix_web::http::StatusCode { if self.error_type == LemmyErrorType::IncorrectLogin { - return http::StatusCode::UNAUTHORIZED; + return actix_web::http::StatusCode::UNAUTHORIZED; } match self.inner.downcast_ref::() { - Some(diesel::result::Error::NotFound) => http::StatusCode::NOT_FOUND, - _ => http::StatusCode::BAD_REQUEST, + Some(diesel::result::Error::NotFound) => actix_web::http::StatusCode::NOT_FOUND, + _ => actix_web::http::StatusCode::BAD_REQUEST, } } @@ -253,7 +260,18 @@ cfg_if! { LemmyError { error_type, inner, - context: SpanTrace::capture(), + context: Backtrace::capture(), + } + } + } + + 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(), } } } @@ -267,7 +285,7 @@ cfg_if! { self.map_err(|error| LemmyError { error_type, inner: error.into(), - context: SpanTrace::capture(), + context: Backtrace::capture(), }) } } @@ -291,7 +309,6 @@ cfg_if! { #[cfg(test)] mod tests { - #![allow(clippy::unwrap_used)] #![allow(clippy::indexing_slicing)] use super::*; use actix_web::{body::MessageBody, ResponseError}; @@ -300,39 +317,57 @@ cfg_if! { use strum::IntoEnumIterator; #[test] - fn deserializes_no_message() { + fn deserializes_no_message() -> LemmyResult<()> { let err = LemmyError::from(LemmyErrorType::Banned).error_response(); - let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); - assert_eq!(&json, "{\"error\":\"banned\"}") + let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?; + assert_eq!(&json, "{\"error\":\"banned\"}"); + + Ok(()) } #[test] - fn deserializes_with_message() { - let reg_banned = LemmyErrorType::PersonIsBannedFromSite(String::from("reason")); + fn deserializes_with_message() -> LemmyResult<()> { + 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().to_vec()).unwrap(); + 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(()) + } + + #[test] + fn test_convert_diesel_errors() { + let not_found_error = LemmyError::from(diesel::NotFound); + assert_eq!(LemmyErrorType::NotFound, not_found_error.error_type); + assert_eq!(404, not_found_error.status_code()); + + let other_error = LemmyError::from(diesel::result::Error::NotInTransaction); + assert!(matches!(other_error.error_type, LemmyErrorType::Unknown{..})); + assert_eq!(400, other_error.status_code()); } /// Check if errors match translations. Disabled because many are not translated at all. #[test] #[ignore] - fn test_translations_match() { + fn test_translations_match() -> LemmyResult<()> { #[derive(Deserialize)] struct Err { error: String, } - let translations = read_to_string("translations/translations/en.json").unwrap(); - LemmyErrorType::iter().for_each(|e| { - let msg = serde_json::to_string(&e).unwrap(); - let msg: Err = serde_json::from_str(&msg).unwrap(); + let translations = read_to_string("translations/translations/en.json")?; + + for e in LemmyErrorType::iter() { + let msg = serde_json::to_string(&e)?; + let msg: Err = serde_json::from_str(&msg)?; let msg = msg.error; assert!(translations.contains(&format!("\"{msg}\"")), "{msg}"); - }); + } + + Ok(()) } } } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 5a5e76d2a..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; @@ -29,6 +28,8 @@ pub const CACHE_DURATION_FEDERATION: Duration = Duration::from_secs(60); pub const CACHE_DURATION_API: Duration = Duration::from_secs(1); +pub const MAX_COMMENT_DEPTH_LIMIT: usize = 50; + #[macro_export] macro_rules! location_info { () => { diff --git a/crates/utils/src/rate_limit/mod.rs b/crates/utils/src/rate_limit/mod.rs index de64bac46..a6cf92150 100644 --- a/crates/utils/src/rate_limit/mod.rs +++ b/crates/utils/src/rate_limit/mod.rs @@ -221,8 +221,6 @@ fn parse_ip(addr: &str) -> Option { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { #[test] diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index a3c6f6a27..01d379986 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -136,7 +136,6 @@ impl MapLevel for Map { .entry(addr_part) .or_insert(RateLimitedGroup::new(now, adjusted_configs)); - #[allow(clippy::indexing_slicing)] let total_passes = group.check_total(action_type, now, adjusted_configs[action_type]); let children_pass = group.children.check( @@ -161,7 +160,6 @@ impl MapLevel for Map { // Evaluated if `some_children_remaining` is false let total_has_refill_in_future = || { group.total.into_iter().any(|(action_type, bucket)| { - #[allow(clippy::indexing_slicing)] let config = configs[action_type]; bucket.update(now, config).tokens != config.capacity }) @@ -214,7 +212,6 @@ impl RateLimitedGroup { now: InstantSecs, config: BucketConfig, ) -> bool { - #[allow(clippy::indexing_slicing)] // `EnumMap` has no `get` function let bucket = &mut self.total[action_type]; let new_bucket = bucket.update(now, config); @@ -311,11 +308,10 @@ fn split_ipv6(ip: Ipv6Addr) -> ([u8; 6], u8, u8) { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::{ActionType, BucketConfig, InstantSecs, RateLimitState, RateLimitedGroup}; + use crate::error::LemmyResult; use pretty_assertions::assert_eq; #[test] @@ -330,7 +326,7 @@ mod tests { } #[test] - fn test_rate_limiter() { + fn test_rate_limiter() -> LemmyResult<()> { let bucket_configs = enum_map::enum_map! { ActionType::Message => BucketConfig { capacity: 2, @@ -354,14 +350,13 @@ mod tests { "1:2:3:0405:6::", ]; for ip in ips { - let ip = ip.parse().unwrap(); + let ip = ip.parse()?; let message_passed = rate_limiter.check(ActionType::Message, ip, now); let post_passed = rate_limiter.check(ActionType::Post, ip, now); assert!(message_passed); assert!(post_passed); } - #[allow(clippy::indexing_slicing)] let expected_buckets = |factor: u32, tokens_consumed: u32| { let adjusted_configs = bucket_configs.map(|_, config| BucketConfig { capacity: config.capacity.saturating_mul(factor), @@ -412,7 +407,7 @@ mod tests { // Do 2 `Message` actions for 1 IP address and expect only the 2nd one to fail for expected_to_pass in [true, false] { - let ip = "1:2:3:0400::".parse().unwrap(); + let ip = "1:2:3:0400::".parse()?; let passed = rate_limiter.check(ActionType::Message, ip, now); assert_eq!(passed, expected_to_pass); } @@ -424,7 +419,7 @@ mod tests { assert!(rate_limiter.ipv6_buckets.is_empty()); // `remove full buckets` should not remove empty buckets - let ip = "1.1.1.1".parse().unwrap(); + let ip = "1.1.1.1".parse()?; // empty the bucket with 2 requests assert!(rate_limiter.check(ActionType::Post, ip, now)); assert!(rate_limiter.check(ActionType::Post, ip, now)); @@ -434,11 +429,13 @@ mod tests { // `remove full buckets` should not remove partial buckets now.secs += 2; - let ip = "1.1.1.1".parse().unwrap(); + let ip = "1.1.1.1".parse()?; // Only make one request, so bucket still has 1 token assert!(rate_limiter.check(ActionType::Post, ip, now)); rate_limiter.remove_full_buckets(now); assert!(!rate_limiter.ipv4_buckets.is_empty()); + + Ok(()) } } diff --git a/crates/utils/src/response.rs b/crates/utils/src/response.rs index 82b1e70ed..f37c15dd7 100644 --- a/crates/utils/src/response.rs +++ b/crates/utils/src/response.rs @@ -37,6 +37,7 @@ mod tests { use crate::error::{LemmyError, LemmyErrorType}; use actix_web::{ error::ErrorInternalServerError, + http::StatusCode, middleware::ErrorHandlers, test, web, @@ -45,7 +46,6 @@ mod tests { Handler, Responder, }; - use http::StatusCode; use pretty_assertions::assert_eq; #[actix_web::test] diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 547ae20d9..c95f66644 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -90,6 +90,10 @@ pub struct PictrsConfig { /// Timeout for uploading images to pictrs (in seconds) #[default(30)] pub upload_timeout: u64, + + /// Resize post thumbnails to this maximum width/height. + #[default(512)] + pub max_thumbnail_size: u32, } #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document, PartialEq)] diff --git a/crates/utils/src/utils/markdown/image_links.rs b/crates/utils/src/utils/markdown/image_links.rs new file mode 100644 index 000000000..7456190e4 --- /dev/null +++ b/crates/utils/src/utils/markdown/image_links.rs @@ -0,0 +1,165 @@ +use super::{link_rule::Link, MARKDOWN_PARSER}; +use crate::settings::SETTINGS; +use markdown_it::{plugins::cmark::inline::image::Image, NodeValue}; +use url::Url; +use urlencoding::encode; + +/// Rewrites all links to remote domains in markdown, so they go through `/api/v3/image_proxy`. +pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { + let links_offsets = find_urls::(&src); + + let mut links = vec![]; + // Go through the collected links in reverse order + for (start, end) in links_offsets.into_iter().rev() { + let (url, extra) = markdown_handle_title(&src, start, end); + match Url::parse(url) { + Ok(parsed) => { + links.push(parsed.clone()); + // If link points to remote domain, replace with proxied link + if parsed.domain() != Some(&SETTINGS.hostname) { + let mut proxied = format!( + "{}/api/v3/image_proxy?url={}", + SETTINGS.get_protocol_and_hostname(), + encode(url), + ); + // restore custom emoji format + if let Some(extra) = extra { + proxied = format!("{proxied} {extra}"); + } + src.replace_range(start..end, &proxied); + } + } + Err(_) => { + // If its not a valid url, replace with empty text + src.replace_range(start..end, ""); + } + } + } + + (src, links) +} + +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")` + match content.split_once(' ') { + Some((a, b)) => (a, Some(b)), + _ => (content, None), + } +} + +pub fn markdown_find_links(src: &str) -> Vec<(usize, usize)> { + find_urls::(src) +} + +// Walk the syntax tree to find positions of image or link urls +fn find_urls(src: &str) -> Vec<(usize, usize)> { + let ast = MARKDOWN_PARSER.parse(src); + let mut links_offsets = vec![]; + ast.walk(|node, _depth| { + if let Some(image) = node.cast::() { + 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)); + } + }); + links_offsets +} + +pub trait UrlAndTitle { + fn url_len(&self) -> usize; + fn title_len(&self) -> usize; +} + +impl UrlAndTitle for Image { + fn url_len(&self) -> usize { + self.url.len() + } + + fn title_len(&self) -> usize { + self.title.as_ref().map(|t| t.len() + 3).unwrap_or_default() + } +} +impl UrlAndTitle for Link { + fn url_len(&self) -> usize { + self.url.len() + } + fn title_len(&self) -> usize { + self.title.as_ref().map(|t| t.len() + 3).unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_find_links() { + let links = markdown_find_links("[test](https://example.com)"); + assert_eq!(vec![(7, 26)], links); + + let links = find_urls::("![test](https://example.com)"); + assert_eq!(vec![(8, 27)], links); + } + + #[test] + fn test_markdown_proxy_images() { + let tests: Vec<_> = + vec![ + ( + "remote image proxied", + "![link](http://example.com/image.jpg)", + "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", + ), + ( + "local image unproxied", + "![link](http://lemmy-alpha/image.jpg)", + "![link](http://lemmy-alpha/image.jpg)", + ), + ( + "multiple image links", + "![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)", + "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", + ), + ( + "empty link handled", + "![image]()", + "![image]()" + ), + ( + "empty label handled", + "![](http://example.com/image.jpg)", + "![](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" + ), + ( + "invalid image link removed", + "![image](http-not-a-link)", + "![image]()" + ), + ( + "label with nested markdown handled", + "![a *b* c](http://example.com/image.jpg)", + "![a *b* c](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" + ), + ( + "custom emoji support", + r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#, + r#"![party-blob](https://lemmy-alpha/api/v3/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# + ) + ]; + + tests.iter().for_each(|&(msg, input, expected)| { + let result = markdown_rewrite_image_links(input.to_string()); + + assert_eq!( + result.0, expected, + "Testing {}, with original input '{}'", + msg, input + ); + }); + } +} diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs index 7ed553e06..9d34e8a69 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -1,18 +1,19 @@ -use crate::{error::LemmyResult, settings::SETTINGS, LemmyErrorType}; -use markdown_it::{plugins::cmark::inline::image::Image, MarkdownIt}; +use crate::error::{LemmyErrorType, LemmyResult}; +use markdown_it::MarkdownIt; use regex::RegexSet; use std::sync::LazyLock; -use url::Url; -use urlencoding::encode; +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 @@ -35,70 +36,6 @@ pub fn markdown_to_html(text: &str) -> String { MARKDOWN_PARSER.parse(text).xrender() } -/// Rewrites all links to remote domains in markdown, so they go through `/api/v3/image_proxy`. -pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { - let ast = MARKDOWN_PARSER.parse(&src); - let mut links_offsets = vec![]; - - // Walk the syntax tree to find positions of image links - ast.walk(|node, _depth| { - if let Some(image) = node.cast::() { - // srcmap is always present for image - // https://github.com/markdown-it-rust/markdown-it/issues/36#issuecomment-1777844387 - let node_offsets = node.srcmap.expect("srcmap is none").get_byte_offsets(); - // necessary for custom emojis which look like `![name](url "title")` - let start_offset = node_offsets.1 - - image.url.len() - - 1 - - image - .title - .as_ref() - .map(|t| t.len() + 3) - .unwrap_or_default(); - let end_offset = node_offsets.1 - 1; - - links_offsets.push((start_offset, end_offset)); - } - }); - - let mut links = vec![]; - // Go through the collected links in reverse order - while let Some((start, end)) = links_offsets.pop() { - 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) - }; - match Url::parse(url) { - Ok(parsed) => { - links.push(parsed.clone()); - // If link points to remote domain, replace with proxied link - if parsed.domain() != Some(&SETTINGS.hostname) { - let mut proxied = format!( - "{}/api/v3/image_proxy?url={}", - SETTINGS.get_protocol_and_hostname(), - encode(url), - ); - // restore custom emoji format - if let Some(extra) = extra { - proxied = format!("{proxied} {extra}"); - } - src.replace_range(start..end, &proxied); - } - } - Err(_) => { - // If its not a valid url, replace with empty text - src.replace_range(start..end, ""); - } - } - } - - (src, links) -} - pub fn markdown_check_for_blocked_urls(text: &str, blocklist: &RegexSet) -> LemmyResult<()> { if blocklist.is_match(text) { Err(LemmyErrorType::BlockedUrl)? @@ -107,11 +44,10 @@ pub fn markdown_check_for_blocked_urls(text: &str, blocklist: &RegexSet) -> Lemm } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; + use image_links::markdown_rewrite_image_links; use pretty_assertions::assert_eq; #[test] @@ -168,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" ) ]; @@ -246,8 +192,8 @@ mod tests { } #[test] - fn test_url_blocking() { - let set = RegexSet::new(vec![r"(https://)?example\.com/?"]).unwrap(); + fn test_url_blocking() -> LemmyResult<()> { + let set = RegexSet::new(vec![r"(https://)?example\.com/?"])?; assert!( markdown_check_for_blocked_urls(&String::from("[](https://example.com)"), &set).is_err() @@ -275,7 +221,7 @@ mod tests { ) .is_err()); - let set = RegexSet::new(vec![r"(https://)?example\.com/spam\.jpg"]).unwrap(); + let set = RegexSet::new(vec![r"(https://)?example\.com/spam\.jpg"])?; assert!(markdown_check_for_blocked_urls( &String::from("![](https://example.com/spam.jpg)"), &set @@ -286,8 +232,7 @@ mod tests { r"(https://)?quo\.example\.com/?", r"(https://)?foo\.example\.com/?", r"(https://)?bar\.example\.com/?", - ]) - .unwrap(); + ])?; assert!( markdown_check_for_blocked_urls(&String::from("https://baz.example.com"), &set).is_ok() @@ -297,15 +242,17 @@ mod tests { markdown_check_for_blocked_urls(&String::from("https://bar.example.com"), &set).is_err() ); - let set = RegexSet::new(vec![r"(https://)?example\.com/banned_page"]).unwrap(); + let set = RegexSet::new(vec![r"(https://)?example\.com/banned_page"])?; assert!( markdown_check_for_blocked_urls(&String::from("https://example.com/page"), &set).is_ok() ); - let set = RegexSet::new(vec![r"(https://)?ex\.mple\.com/?"]).unwrap(); + let set = RegexSet::new(vec![r"(https://)?ex\.mple\.com/?"])?; assert!(markdown_check_for_blocked_urls("example.com", &set).is_ok()); + + Ok(()) } #[test] 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 caced310a..000000000 --- a/crates/utils/src/utils/markdown/spoiler_rule.rs +++ /dev/null @@ -1,204 +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)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] -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/mention.rs b/crates/utils/src/utils/mention.rs index c7cc2043f..13762ed27 100644 --- a/crates/utils/src/utils/mention.rs +++ b/crates/utils/src/utils/mention.rs @@ -34,8 +34,7 @@ pub fn scrape_text_for_mentions(text: &str) -> Vec { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod test { use crate::utils::mention::scrape_text_for_mentions; diff --git a/crates/utils/src/utils/slurs.rs b/crates/utils/src/utils/slurs.rs index ba94372fa..2350822eb 100644 --- a/crates/utils/src/utils/slurs.rs +++ b/crates/utils/src/utils/slurs.rs @@ -61,17 +61,18 @@ pub(crate) fn slurs_vec_to_str(slurs: &[&str]) -> String { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod test { - use crate::utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str}; + use crate::{ + error::LemmyResult, + utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str}, + }; use pretty_assertions::assert_eq; use regex::RegexBuilder; #[test] - fn test_slur_filter() { - let slur_regex = Some(RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap()); + fn test_slur_filter() -> LemmyResult<()> { + let slur_regex = Some(RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build()?); let test = "faggot test kike tranny cocksucker retardeds. Capitalized Niggerz. This is a bunch of other safe text."; let slur_free = "No slurs here"; @@ -96,6 +97,8 @@ mod test { if let Err(slur_vec) = slur_check(test, &slur_regex) { assert_eq!(&slurs_vec_to_str(&slur_vec), has_slurs_err_str); } + + Ok(()) } // These helped with testing diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index 0a59e2fea..f8da6f609 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -1,4 +1,5 @@ use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +use clearurls::UrlCleaner; use itertools::Itertools; use regex::{Regex, RegexBuilder, RegexSet}; use std::sync::LazyLock; @@ -10,17 +11,13 @@ static VALID_MATRIX_ID_REGEX: LazyLock = LazyLock::new(|| { .expect("compile regex") }); // taken from https://en.wikipedia.org/wiki/UTM_parameters -static CLEAN_URL_PARAMS_REGEX: LazyLock = LazyLock::new(|| { - Regex::new( - r"^(utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|gclsrc|dclid|fbclid)=", - ) - .expect("compile regex") -}); +static URL_CLEANER: LazyLock = + LazyLock::new(|| UrlCleaner::from_embedded_rules().expect("compile clearurls")); const ALLOWED_POST_URL_SCHEMES: [&str; 3] = ["http", "https", "magnet"]; const BODY_MAX_LENGTH: usize = 10000; const POST_BODY_MAX_LENGTH: usize = 50000; -const BIO_MAX_LENGTH: usize = 300; +const BIO_MAX_LENGTH: usize = 1000; const URL_MAX_LENGTH: usize = 2000; const ALT_TEXT_MAX_LENGTH: usize = 1500; const SITE_NAME_MAX_LENGTH: usize = 20; @@ -194,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, @@ -257,16 +254,22 @@ pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> LemmyResult