diff --git a/.woodpecker.yml b/.woodpecker.yml index 00798f081..50403711e 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -2,9 +2,14 @@ # See https://github.com/woodpecker-ci/woodpecker/issues/1677 variables: - - &rust_image "rust:1.78" + - &rust_image "rust:1.80" - &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" + - install_diesel_cli: &install_diesel_cli + - apt-get update && apt-get install -y postgresql-client + - cargo install diesel_cli --no-default-features --features postgres + - export PATH="$CARGO_HOME/bin:$PATH" - &slow_check_paths - event: pull_request path: @@ -25,17 +30,6 @@ variables: "diesel.toml", ".gitmodules", ] - - install_binstall: &install_binstall - - wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz - - tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz - - cp cargo-binstall /usr/local/cargo/bin - - install_diesel_cli: &install_diesel_cli - - apt update && apt install -y lsb-release build-essential - - sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' - - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - - - apt update && apt install -y postgresql-client-16 - - cargo install diesel_cli --no-default-features --features postgres - - export PATH="$CARGO_HOME/bin:$PATH" steps: prepare_repo: @@ -82,7 +76,7 @@ steps: cargo_machete: image: *rust_nightly_image commands: - - <<: *install_binstall + - *install_binstall - cargo binstall -y cargo-machete - cargo machete when: @@ -204,11 +198,6 @@ steps: - <<: *install_diesel_cli # Run all migrations - diesel migration run - # Dump schema to before.sqldump (PostgreSQL apt repo is used to prevent pg_dump version mismatch error) - - apt update && apt install -y lsb-release - - sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' - - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - - - apt update && apt install -y postgresql-client-16 - psql -c "DROP SCHEMA IF EXISTS r CASCADE;" - pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f before.sqldump # Make sure that the newest migration is revertable without the `r` schema @@ -232,7 +221,7 @@ steps: DO_WRITE_HOSTS_FILE: "1" commands: - *install_pnpm - - apt update && apt install -y bash curl postgresql-client + - apt-get update && apt-get install -y bash curl postgresql-client - bash api_tests/prepare-drone-federation-test.sh - cd api_tests/ - pnpm i @@ -279,7 +268,7 @@ steps: publish_to_crates_io: image: *rust_image commands: - - <<: *install_binstall + - *install_binstall # Install cargo-workspaces - cargo binstall -y cargo-workspaces - cp -r migrations crates/db_schema/ @@ -307,7 +296,8 @@ steps: services: database: - image: postgres:16-alpine + # 15-alpine image necessary because of diesel tests + image: pgautoupgrade/pgautoupgrade:15-alpine environment: POSTGRES_USER: lemmy POSTGRES_PASSWORD: password diff --git a/Cargo.lock b/Cargo.lock index 4f8f5992b..aad3ab169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,9 +16,9 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772" [[package]] name = "activitypub_federation" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8ff2d0151ce9ac02eb29e4a58b41d28693f141f7963d4bfabd2f9d402ecec7" +checksum = "b86eea7a032da501fe07b04a83c10716ea732c45e6943d7f045bc053aca03f2a" dependencies = [ "activitystreams-kinds", "actix-web", @@ -35,14 +35,15 @@ dependencies = [ "http-signature-normalization", "http-signature-normalization-reqwest", "httpdate", - "itertools 0.12.1", + "itertools 0.13.0", "moka", "once_cell", - "openssl", "pin-project-lite", + "rand", "regex", "reqwest 0.11.27", "reqwest-middleware 0.2.5", + "rsa", "serde", "serde_json", "sha2", @@ -218,7 +219,7 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio", + "mio 0.8.11", "socket2", "tokio", "tracing", @@ -266,9 +267,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.7.0" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6316df3fa569627c98b12557a8b6ff0674e5be4bb9b5e4ae2550ddb4964ed6" +checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" dependencies = [ "actix-codec", "actix-http", @@ -289,6 +290,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", + "impl-more", "itoa", "language-tags", "log", @@ -437,9 +439,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" @@ -542,9 +544,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", @@ -564,12 +566,6 @@ dependencies = [ "quick-xml 0.31.0", ] -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -584,9 +580,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.7.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474d7cec9d0a1126fad1b224b767fcbf351c23b0309bb21ec210bcfd379926a5" +checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -597,9 +593,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.17.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7505fc3cb7acbf42699a43a79dd9caa4ed9e99861dfbb837c5c0fb5a0a8d2980" +checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9" dependencies = [ "bindgen", "cc", @@ -617,7 +613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", @@ -632,7 +628,34 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "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.0", + "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", @@ -655,6 +678,26 @@ dependencies = [ "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.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -870,9 +913,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "bytestring" @@ -899,13 +942,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -977,9 +1020,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.7" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -987,9 +1030,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -999,9 +1042,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1115,21 +1158,21 @@ checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" dependencies = [ "futures-core", "prost 0.12.6", - "prost-types", + "prost-types 0.12.6", "tonic 0.10.2", "tracing-core", ] [[package]] name = "console-api" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a257c22cd7e487dd4a13d413beabc512c5052f0bc048db0da6a84c3d8a6142fd" +checksum = "86ed14aa9c9f927213c6e4f3ef75faaad3406134efe84ba2cb7983431d5f0931" dependencies = [ "futures-core", - "prost 0.12.6", - "prost-types", - "tonic 0.11.0", + "prost 0.13.2", + "prost-types 0.13.2", + "tonic 0.12.2", "tracing-core", ] @@ -1145,7 +1188,7 @@ dependencies = [ "futures-task", "hdrhistogram", "humantime", - "prost-types", + "prost-types 0.12.6", "serde", "serde_json", "thread_local", @@ -1159,24 +1202,25 @@ dependencies = [ [[package]] name = "console-subscriber" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c4cc54bae66f7d9188996404abdf7fdfa23034ef8e43478c8810828abad758" +checksum = "e2e3a111a37f3333946ebf9da370ba5c5577b18eb342ec683eb488dd21980302" dependencies = [ - "console-api 0.7.0", + "console-api 0.8.0", "crossbeam-channel", "crossbeam-utils", "futures-task", "hdrhistogram", "humantime", - "prost 0.12.6", - "prost-types", + "hyper-util", + "prost 0.13.2", + "prost-types 0.13.2", "serde", "serde_json", "thread_local", "tokio", "tokio-stream", - "tonic 0.11.0", + "tonic 0.12.2", "tracing", "tracing-core", "tracing-subscriber", @@ -1462,9 +1506,9 @@ 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", @@ -1635,6 +1679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1648,17 +1693,6 @@ dependencies = [ "chrono", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "doku" version = "0.21.1" @@ -2213,17 +2247,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "hostname" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" -dependencies = [ - "cfg-if", - "libc", - "windows", -] - [[package]] name = "hound" version = "3.5.1" @@ -2420,9 +2443,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -2439,6 +2462,20 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.29", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.26.0" @@ -2447,7 +2484,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "rustls 0.22.4", "rustls-pki-types", @@ -2468,6 +2505,19 @@ dependencies = [ "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" @@ -2483,16 +2533,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.3.1", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -2544,124 +2594,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -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.66", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2688,18 +2620,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "idna" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" -dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", -] - [[package]] name = "image" version = "0.24.9" @@ -2874,6 +2794,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "lazycell" @@ -2883,7 +2806,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lemmy_api" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -2896,6 +2819,7 @@ dependencies = [ "elementtree", "hound", "lemmy_api_common", + "lemmy_api_crud", "lemmy_db_schema", "lemmy_db_views", "lemmy_db_views_actor", @@ -2912,7 +2836,7 @@ dependencies = [ [[package]] name = "lemmy_api_common" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -2930,7 +2854,6 @@ dependencies = [ "lemmy_utils", "mime", "moka", - "once_cell", "pretty_assertions", "regex", "reqwest 0.11.27", @@ -2950,7 +2873,7 @@ dependencies = [ [[package]] name = "lemmy_api_crud" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "accept-language", "activitypub_federation", @@ -2964,7 +2887,6 @@ dependencies = [ "lemmy_db_views_actor", "lemmy_utils", "moka", - "once_cell", "tracing", "url", "uuid", @@ -2973,7 +2895,7 @@ dependencies = [ [[package]] name = "lemmy_apub" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -2994,7 +2916,6 @@ dependencies = [ "lemmy_db_views_actor", "lemmy_utils", "moka", - "once_cell", "pretty_assertions", "reqwest 0.11.27", "serde", @@ -3002,7 +2923,7 @@ dependencies = [ "serde_with", "serial_test", "stringreader", - "strum_macros", + "strum", "tokio", "tracing", "url", @@ -3011,7 +2932,7 @@ dependencies = [ [[package]] name = "lemmy_db_perf" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "anyhow", "clap", @@ -3026,7 +2947,7 @@ dependencies = [ [[package]] name = "lemmy_db_schema" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "anyhow", @@ -3045,16 +2966,14 @@ dependencies = [ "i-love-jesus", "lemmy_utils", "moka", - "once_cell", "pretty_assertions", "regex", - "rustls 0.23.10", + "rustls 0.23.12", "serde", "serde_json", "serde_with", "serial_test", "strum", - "strum_macros", "tokio", "tokio-postgres", "tokio-postgres-rustls", @@ -3067,7 +2986,7 @@ dependencies = [ [[package]] name = "lemmy_db_views" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "actix-web", "chrono", @@ -3089,7 +3008,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_actor" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "chrono", "diesel", @@ -3102,7 +3021,6 @@ dependencies = [ "serde_with", "serial_test", "strum", - "strum_macros", "tokio", "ts-rs", "url", @@ -3110,7 +3028,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_moderator" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "diesel", "diesel-async", @@ -3122,7 +3040,7 @@ dependencies = [ [[package]] name = "lemmy_federate" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -3139,7 +3057,6 @@ dependencies = [ "lemmy_utils", "mockall", "moka", - "once_cell", "reqwest 0.11.27", "serde_json", "serial_test", @@ -3154,7 +3071,7 @@ dependencies = [ [[package]] name = "lemmy_routes" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-web", @@ -3166,7 +3083,6 @@ dependencies = [ "lemmy_db_views", "lemmy_db_views_actor", "lemmy_utils", - "once_cell", "reqwest 0.11.27", "reqwest-middleware 0.2.5", "rss", @@ -3179,7 +3095,7 @@ dependencies = [ [[package]] name = "lemmy_server" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "activitypub_federation", "actix-cors", @@ -3188,7 +3104,7 @@ dependencies = [ "chrono", "clap", "clokwerk", - "console-subscriber 0.3.0", + "console-subscriber 0.4.0", "diesel", "diesel-async", "futures-util", @@ -3208,7 +3124,7 @@ dependencies = [ "reqwest 0.11.27", "reqwest-middleware 0.2.5", "reqwest-tracing 0.4.8", - "rustls 0.23.10", + "rustls 0.23.12", "serde_json", "serial_test", "tokio", @@ -3223,7 +3139,7 @@ dependencies = [ [[package]] name = "lemmy_utils" -version = "0.19.5" +version = "0.19.6-beta.7" dependencies = [ "actix-web", "anyhow", @@ -3238,8 +3154,6 @@ dependencies = [ "itertools 0.13.0", "lettre", "markdown-it", - "once_cell", - "openssl", "pretty_assertions", "regex", "reqwest 0.11.27", @@ -3250,7 +3164,6 @@ dependencies = [ "serde_json", "smart-default", "strum", - "strum_macros", "tokio", "tracing", "tracing-error", @@ -3274,18 +3187,19 @@ dependencies = [ "fastrand", "futures-io", "futures-util", - "hostname", "httpdate", "idna 0.5.0", "mime", - "native-tls", "nom", "percent-encoding", "quoted_printable", + "rustls 0.23.12", + "rustls-pemfile 2.1.2", "socket2", "tokio", - "tokio-native-tls", + "tokio-rustls 0.26.0", "url", + "webpki-roots 0.26.2", ] [[package]] @@ -3304,6 +3218,12 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "line-wrap" version = "0.2.0" @@ -3331,12 +3251,6 @@ 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" @@ -3391,9 +3305,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "markdown-it" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "181d1b4704b0a2fc6027d3f758a7eedb975dcbaab86e6794901b8cfc52af94d5" +checksum = "f99c010929c8217b2dc0940954267a2e15a15f17cb309cd1f299e21933f84fac" dependencies = [ "argparse", "const_format", @@ -3530,7 +3444,7 @@ checksum = "26eb45aff37b45cff885538e1dcbd6c2b462c04fe84ce0155ea469f325672c98" dependencies = [ "base64 0.22.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "indexmap 2.2.6", "ipnet", @@ -3622,6 +3536,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "mirai-annotations" version = "1.12.0" @@ -3630,14 +3556,13 @@ checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" [[package]] name = "mockall" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" dependencies = [ "cfg-if", "downcast", "fragile", - "lazy_static", "mockall_derive", "predicates", "predicates-tree", @@ -3645,9 +3570,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" dependencies = [ "cfg-if", "proc-macro2", @@ -3657,9 +3582,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e0d88686dc561d743b40de8269b26eaf0dc58781bde087b0984646602021d08" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" dependencies = [ "async-lock", "async-trait", @@ -3750,6 +3675,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3765,6 +3707,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3772,6 +3725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4228,7 +4182,7 @@ dependencies = [ "reqwest 0.12.4", "reqwest-middleware 0.3.1", "reqwest-tracing 0.5.0", - "rustls 0.23.10", + "rustls 0.23.12", "rustls-channel-resolver", "rustls-pemfile 2.1.2", "rusty-s3", @@ -4255,7 +4209,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", - "webpki-roots", + "webpki-roots 0.26.2", ] [[package]] @@ -4290,6 +4244,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -4345,11 +4320,11 @@ dependencies = [ [[package]] name = "postgres-protocol" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" +checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "byteorder", "bytes", "fallible-iterator", @@ -4363,9 +4338,9 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +checksum = "02048d9e032fb3cc3413bbf7b83a15d84a5d419778e2628751896d856498eee9" dependencies = [ "bytes", "fallible-iterator", @@ -4518,6 +4493,16 @@ dependencies = [ "prost-derive 0.12.6", ] +[[package]] +name = "prost" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +dependencies = [ + "bytes", + "prost-derive 0.13.2", +] + [[package]] name = "prost-derive" version = "0.11.9" @@ -4544,6 +4529,19 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "prost-derive" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "prost-types" version = "0.12.6" @@ -4553,6 +4551,15 @@ dependencies = [ "prost 0.12.6", ] +[[package]] +name = "prost-types" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" +dependencies = [ + "prost 0.13.2", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -4807,6 +4814,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.29", + "hyper-rustls 0.24.2", "hyper-tls", "ipnet", "js-sys", @@ -4817,14 +4825,16 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", @@ -4832,6 +4842,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots 0.25.4", "winreg 0.50.0", ] @@ -4848,8 +4859,8 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.3.1", - "hyper-rustls", + "hyper 1.4.1", + "hyper-rustls 0.26.0", "hyper-util", "ipnet", "js-sys", @@ -4864,7 +4875,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "tokio", "tokio-rustls 0.25.0", "tokio-util", @@ -4874,7 +4885,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 0.26.2", "winreg 0.52.0", ] @@ -4967,7 +4978,7 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin", + "spin 0.9.8", "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -5004,6 +5015,26 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f8c01b9158de3aa5a7ac041a41c0e854d7adc3e473e7d7e2143eb5432bc5ba2" +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rss" version = "2.0.8" @@ -5050,6 +5081,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.22.4" @@ -5059,23 +5102,23 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] @@ -5087,7 +5130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fede2a247359da6b4998f7723ec6468c2d6a577a5d8c17e54f21806426ad2290" dependencies = [ "nanorand", - "rustls 0.23.10", + "rustls 0.23.12", ] [[package]] @@ -5117,9 +5160,19 @@ checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "aws-lc-rs", "ring", @@ -5201,6 +5254,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + [[package]] name = "sdd" version = "0.2.0" @@ -5249,9 +5312,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -5267,9 +5330,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -5278,12 +5341,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "indexmap 2.2.6", "itoa", + "memchr", "ryu", "serde", ] @@ -5320,9 +5384,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64 0.22.1", "chrono", @@ -5338,9 +5402,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ "darling 0.20.9", "proc-macro2", @@ -5425,6 +5489,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core", ] @@ -5526,6 +5591,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -5542,12 +5613,6 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "stacker" version = "0.1.15" @@ -5634,9 +5699,12 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" @@ -5686,15 +5754,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "synstructure" -version = "0.13.1" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "syntect" @@ -5815,18 +5878,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -5880,16 +5943,6 @@ 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.6.0" @@ -5928,22 +5981,21 @@ dependencies = [ [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.2", "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5958,9 +6010,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -5979,9 +6031,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" +checksum = "03adcf0147e203b6032c0b2d30be1415ba03bc348901f3ff1cc0df6a733e60c3" dependencies = [ "async-trait", "byteorder", @@ -6010,7 +6062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8e98c31c29b2666fb28720739e11476166be4ead1610a37dcd7414bb124413a" dependencies = [ "aws-lc-rs", - "rustls 0.23.10", + "rustls 0.23.12", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", @@ -6024,13 +6076,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" dependencies = [ "ring", - "rustls 0.23.10", + "rustls 0.23.12", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", "x509-certificate", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.25.0" @@ -6048,7 +6110,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.10", + "rustls 0.23.12", "rustls-pki-types", "tokio", ] @@ -6144,7 +6206,7 @@ checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.13.1", "bytes", "futures-core", @@ -6153,7 +6215,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.29", - "hyper-timeout", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", "prost 0.11.9", @@ -6176,14 +6238,14 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", - "axum", + "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.29", - "hyper-timeout", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", "prost 0.12.6", @@ -6203,14 +6265,14 @@ checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" dependencies = [ "async-stream", "async-trait", - "axum", + "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.29", - "hyper-timeout", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", "prost 0.12.6", @@ -6223,10 +6285,40 @@ dependencies = [ ] [[package]] -name = "totp-rs" -version = "5.5.1" +name = "tonic" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f" +checksum = "c6f6ba989e4b2c58ae83d862d3a3e27690b6e3ae630d0deb59f3697f32aa88ad" +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.0", + "http-body-util", + "hyper 1.4.1", + "hyper-timeout 0.5.1", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.2", + "socket2", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "totp-rs" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90" dependencies = [ "base32", "constant_time_eq", @@ -6459,9 +6551,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.12" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" [[package]] name = "try-lock" @@ -6495,18 +6587,18 @@ dependencies = [ [[package]] name = "typed-builder" -version = "0.18.2" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add" +checksum = "a06fbd5b8de54c5f7c91f6fe4cebb949be2125d7758e630bb58b1d831dbce600" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.18.2" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" +checksum = "f9534daa9fd3ed0bd911d462a37f172228077e7abf18c18a5f67199d959205f8" dependencies = [ "proc-macro2", "quote", @@ -6587,12 +6679,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna 1.0.0", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -6609,24 +6701,12 @@ 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.1" @@ -6635,11 +6715,10 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ - "atomic", "getrandom", "serde", ] @@ -6820,6 +6899,12 @@ dependencies = [ "url", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.2" @@ -6883,16 +6968,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core", - "windows-targets 0.52.5", -] - [[package]] name = "windows-core" version = "0.52.0" @@ -7079,18 +7154,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "x509-cert" version = "0.2.5" @@ -7165,30 +7228,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "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.66", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.7.34" @@ -7209,27 +7248,6 @@ dependencies = [ "syn 2.0.66", ] -[[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.66", - "synstructure", -] - [[package]] name = "zeroize" version = "1.8.1" @@ -7250,28 +7268,6 @@ dependencies = [ "syn 2.0.66", ] -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "zstd" version = "0.13.1" diff --git a/Cargo.toml b/Cargo.toml index ce6d3357d..38d4adc5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.19.5" +version = "0.19.6-beta.7" edition = "2021" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -86,28 +86,29 @@ suspicious = { level = "deny", priority = -1 } uninlined_format_args = "allow" unused_self = "deny" unwrap_used = "deny" +unimplemented = "deny" [workspace.dependencies] -lemmy_api = { version = "=0.19.5", path = "./crates/api" } -lemmy_api_crud = { version = "=0.19.5", path = "./crates/api_crud" } -lemmy_apub = { version = "=0.19.5", path = "./crates/apub" } -lemmy_utils = { version = "=0.19.5", path = "./crates/utils", default-features = false } -lemmy_db_schema = { version = "=0.19.5", path = "./crates/db_schema" } -lemmy_api_common = { version = "=0.19.5", path = "./crates/api_common" } -lemmy_routes = { version = "=0.19.5", path = "./crates/routes" } -lemmy_db_views = { version = "=0.19.5", path = "./crates/db_views" } -lemmy_db_views_actor = { version = "=0.19.5", path = "./crates/db_views_actor" } -lemmy_db_views_moderator = { version = "=0.19.5", path = "./crates/db_views_moderator" } -lemmy_federate = { version = "=0.19.5", path = "./crates/federate" } -activitypub_federation = { version = "0.5.6", 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.5.8", default-features = false, features = [ "actix-web", ] } diesel = "2.1.6" diesel_migrations = "2.1.0" diesel-async = "0.4.1" -serde = { version = "1.0.203", features = ["derive"] } -serde_with = "3.8.1" -actix-web = { version = "4.6.0", default-features = false, features = [ +serde = { version = "1.0.204", features = ["derive"] } +serde_with = "3.9.0" +actix-web = { version = "4.8.0", default-features = false, features = [ "macros", "rustls-0_23", "compress-brotli", @@ -120,32 +121,35 @@ tracing-actix-web = { version = "0.7.11", default-features = false } tracing-error = "0.2.0" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -url = { version = "2.5.0", features = ["serde"] } -reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] } +url = { version = "2.5.2", features = ["serde"] } +reqwest = { version = "0.11.27", default-features = false, features = [ + "json", + "blocking", + "gzip", + "rustls-tls", +] } reqwest-middleware = "0.2.5" reqwest-tracing = "0.4.8" clokwerk = "0.4.0" doku = { version = "0.21.1", features = ["url-2"] } bcrypt = "0.15.1" chrono = { version = "0.4.38", features = ["serde"], default-features = false } -serde_json = { version = "1.0.117", features = ["preserve_order"] } +serde_json = { version = "1.0.121", features = ["preserve_order"] } base64 = "0.22.1" -uuid = { version = "1.8.0", features = ["serde", "v4"] } -async-trait = "0.1.80" +uuid = { version = "1.10.0", features = ["serde", "v4"] } +async-trait = "0.1.81" captcha = "0.0.9" 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.18.2" +typed-builder = "0.19.1" serial_test = "3.1.1" -tokio = { version = "1.38.0", features = ["full"] } -regex = "1.10.4" -once_cell = "1.19.0" +tokio = { version = "1.39.2", features = ["full"] } +regex = "1.10.5" diesel-derive-newtype = "2.1.2" diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } -strum = "0.26.2" -strum_macros = "0.26.4" +strum = { version = "0.26.3", features = ["derive"] } itertools = "0.13.0" futures = "0.3.30" http = "0.2.12" @@ -157,17 +161,17 @@ ts-rs = { version = "7.1.1", features = [ "chrono-impl", "no-serde-warnings", ] } -rustls = { version = "0.23.9", features = ["ring"] } +rustls = { version = "0.23.12", features = ["ring"] } futures-util = "0.3.30" -tokio-postgres = "0.7.10" +tokio-postgres = "0.7.11" tokio-postgres-rustls = "0.12.0" urlencoding = "2.1.3" enum-map = "2.7" -moka = { version = "0.12.7", features = ["future"] } +moka = { version = "0.12.8", features = ["future"] } i-love-jesus = { version = "0.1.0" } -clap = { version = "4.5.6", features = ["derive", "env"] } +clap = { version = "4.5.13", features = ["derive", "env"] } pretty_assertions = "1.4.0" -derive-new = "0.6.0" +derive-new = "0.7.0" [dependencies] lemmy_api = { workspace = true } @@ -195,9 +199,10 @@ clokwerk = { workspace = true } serde_json = { workspace = true } tracing-opentelemetry = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true } -console-subscriber = { version = "0.3.0", optional = true } +console-subscriber = { version = "0.4.0", optional = true } opentelemetry-otlp = { version = "0.12.0", optional = true } -pict-rs = { version = "0.5.15", optional = true } +pict-rs = { version = "0.5.16", optional = true } +rustls = { workspace = true } tokio.workspace = true actix-cors = "0.7.0" futures-util = { workspace = true } diff --git a/api_tests/.eslintrc.json b/api_tests/.eslintrc.json deleted file mode 100644 index 75b1706aa..000000000 --- a/api_tests/.eslintrc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "root": true, - "env": { - "browser": true - }, - "plugins": ["@typescript-eslint"], - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "warnOnUnsupportedTypeScriptVersion": false - }, - "rules": { - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/explicit-module-boundary-types": 0, - "@typescript-eslint/no-var-requires": 0, - "arrow-body-style": 0, - "curly": 0, - "eol-last": 0, - "eqeqeq": 0, - "func-style": 0, - "import/no-duplicates": 0, - "max-statements": 0, - "max-params": 0, - "new-cap": 0, - "no-console": 0, - "no-duplicate-imports": 0, - "no-extra-parens": 0, - "no-return-assign": 0, - "no-throw-literal": 0, - "no-trailing-spaces": 0, - "no-unused-expressions": 0, - "no-useless-constructor": 0, - "no-useless-escape": 0, - "no-var": 0, - "prefer-const": 0, - "prefer-rest-params": 0, - "quote-props": 0, - "unicorn/filename-case": 0 - } -} diff --git a/api_tests/eslint.config.mjs b/api_tests/eslint.config.mjs new file mode 100644 index 000000000..cf2c426d0 --- /dev/null +++ b/api_tests/eslint.config.mjs @@ -0,0 +1,56 @@ +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + parser: tseslint.parser, + }, + }, + // For some reason this has to be in its own block + { + ignores: [ + "putTypesInIndex.js", + "dist/*", + "docs/*", + ".yalc", + "jest.config.js", + ], + }, + { + files: ["src/**/*"], + rules: { + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-var-requires": 0, + "arrow-body-style": 0, + curly: 0, + "eol-last": 0, + eqeqeq: 0, + "func-style": 0, + "import/no-duplicates": 0, + "max-statements": 0, + "max-params": 0, + "new-cap": 0, + "no-console": 0, + "no-duplicate-imports": 0, + "no-extra-parens": 0, + "no-return-assign": 0, + "no-throw-literal": 0, + "no-trailing-spaces": 0, + "no-unused-expressions": 0, + "no-useless-constructor": 0, + "no-useless-escape": 0, + "no-var": 0, + "prefer-const": 0, + "prefer-rest-params": 0, + "quote-props": 0, + "unicorn/filename-case": 0, + }, + }, +]; diff --git a/api_tests/package.json b/api_tests/package.json index d80edfdc5..bc2d3ec2d 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -6,9 +6,9 @@ "repository": "https://github.com/LemmyNet/lemmy", "author": "Dessalines", "license": "AGPL-3.0", - "packageManager": "pnpm@9.4.0", + "packageManager": "pnpm@9.9.0", "scripts": { - "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'", + "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-follow": "jest -i follow.spec.ts", @@ -21,16 +21,16 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/node": "^20.12.4", - "@typescript-eslint/eslint-plugin": "^7.5.0", - "@typescript-eslint/parser": "^7.5.0", - "download-file-sync": "^1.0.4", - "eslint": "^9.0.0", + "@types/node": "^22.0.2", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^9.8.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.19.4", + "lemmy-js-client": "0.19.5-alpha.1", "prettier": "^3.2.5", "ts-jest": "^29.1.0", - "typescript": "^5.4.4" + "typescript": "^5.5.4", + "typescript-eslint": "^8.0.0" } } diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index de2c9e84c..3b1970681 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -12,45 +12,41 @@ importers: specifier: ^29.5.12 version: 29.5.12 '@types/node': - specifier: ^20.12.4 - version: 20.14.5 + specifier: ^22.0.2 + version: 22.5.1 '@typescript-eslint/eslint-plugin': - specifier: ^7.5.0 - version: 7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5) + specifier: ^8.0.0 + version: 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) '@typescript-eslint/parser': - specifier: ^7.5.0 - version: 7.13.1(eslint@9.5.0)(typescript@5.4.5) - download-file-sync: - specifier: ^1.0.4 - version: 1.0.4 + specifier: ^8.0.0 + version: 8.0.0(eslint@9.9.1)(typescript@5.5.4) eslint: - specifier: ^9.0.0 - version: 9.5.0 + specifier: ^9.8.0 + version: 9.9.1 eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.1.3(eslint@9.5.0)(prettier@3.3.2) + version: 5.2.1(eslint@9.9.1)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.14.5) + version: 29.7.0(@types/node@22.5.1) lemmy-js-client: - specifier: 0.19.4 - version: 0.19.4 + specifier: 0.19.5-alpha.1 + version: 0.19.5-alpha.1 prettier: specifier: ^3.2.5 - version: 3.3.2 + version: 3.3.3 ts-jest: specifier: ^29.1.0 - version: 29.1.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@20.14.5))(typescript@5.4.5) + 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.5.1))(typescript@5.5.4) typescript: - specifier: ^5.4.4 - version: 5.4.5 + specifier: ^5.5.4 + version: 5.5.4 + typescript-eslint: + specifier: ^8.0.0 + version: 8.0.0(eslint@9.9.1)(typescript@5.5.4) 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'} @@ -228,24 +224,20 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.10.0': - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + '@eslint-community/regexpp@4.11.0': + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-community/regexpp@4.10.1': - resolution: {integrity: sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.16.0': - resolution: {integrity: sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==} + '@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/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.5.0': - resolution: {integrity: sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==} + '@eslint/js@9.9.1': + resolution: {integrity: sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.4': @@ -404,8 +396,8 @@ packages: '@types/jest@29.5.12': resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} - '@types/node@20.14.5': - resolution: {integrity: sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==} + '@types/node@22.5.1': + resolution: {integrity: sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -416,71 +408,70 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@7.13.1': - resolution: {integrity: sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/eslint-plugin@8.0.0': + resolution: {integrity: sha512-STIZdwEQRXAHvNUS6ILDf5z3u95Gc8jzywunxSNqX00OooIemaaNIA0vEgynJlycL5AjabYLLrIyHd4iazyvtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true - '@typescript-eslint/parser@7.13.1': - resolution: {integrity: sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/parser@8.0.0': + resolution: {integrity: sha512-pS1hdZ+vnrpDIxuFXYQpLTILglTjSYJ9MbetZctrUawogUsPdz31DIIRZ9+rab0LhYNTsk88w4fIzVheiTbWOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true - '@typescript-eslint/scope-manager@7.13.1': - resolution: {integrity: sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.0.0': + resolution: {integrity: sha512-V0aa9Csx/ZWWv2IPgTfY7T4agYwJyILESu/PVqFtTFz9RIS823mAze+NbnBI8xiwdX3iqeQbcTYlvB04G9wyQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@7.13.1': - resolution: {integrity: sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@7.13.1': - resolution: {integrity: sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/typescript-estree@7.13.1': - resolution: {integrity: sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/type-utils@8.0.0': + resolution: {integrity: sha512-mJAFP2mZLTBwAn5WI4PMakpywfWFH5nQZezUQdSKV23Pqo6o9iShQg1hP2+0hJJXP2LnZkWPphdIq4juYYwCeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true - '@typescript-eslint/utils@7.13.1': - resolution: {integrity: sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 + '@typescript-eslint/types@8.0.0': + resolution: {integrity: sha512-wgdSGs9BTMWQ7ooeHtu5quddKKs5Z5dS+fHLbrQI+ID0XWJLODGMHRfhwImiHoeO2S5Wir2yXuadJN6/l4JRxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@7.13.1': - resolution: {integrity: sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/typescript-estree@8.0.0': + resolution: {integrity: sha512-5b97WpKMX+Y43YKi4zVcCVLtK5F98dFls3Oxui8LbnmRsseKenbbDinmvxrWegKDMmlkIq/XHuyy0UGLtpCDKg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.0.0': + resolution: {integrity: sha512-k/oS/A/3QeGLRvOWCg6/9rATJL5rec7/5s1YmdS0ZU6LHveJyGFwBvLhSRBv6i9xaj7etmosp+l+ViN1I9Aj/Q==} + 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==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} hasBin: true @@ -521,6 +512,9 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -651,17 +645,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.5: - resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -696,8 +681,10 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - download-file-sync@1.0.4: - resolution: {integrity: sha512-vH92qNH508jZZA12HQNq/aiMDfagr4JvjFiI17Bi8oYjsxwv5ZVIi7iHkYmUXxOQUr90tcVX+8EPePjAqG1Y0w==} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true electron-to-chromium@1.4.648: resolution: {integrity: sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg==} @@ -728,8 +715,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-plugin-prettier@5.1.3: - resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -742,8 +729,8 @@ packages: eslint-config-prettier: optional: true - eslint-scope@8.0.1: - resolution: {integrity: sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==} + eslint-scope@8.0.2: + resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: @@ -754,13 +741,18 @@ packages: resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.5.0: - resolution: {integrity: sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==} + eslint@9.9.1: + resolution: {integrity: sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true - espree@10.0.1: - resolution: {integrity: sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==} + espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: @@ -768,8 +760,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: @@ -822,6 +814,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -925,6 +920,10 @@ packages: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -1006,6 +1005,11 @@ packages: resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} engines: {node: '>=8'} + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1175,8 +1179,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.19.4: - resolution: {integrity: sha512-k3d+YRDj3+JuuEP+nuEg27efR/e4m8oMk2BoC8jq9AnMrwSAKfsN2F2vG70Zke0amXtOclDZrCSHkIpNw99ikg==} + lemmy-js-client@0.19.5-alpha.1: + resolution: {integrity: sha512-GOhaiTQzrpwdmc3DFYemT2SmNmpuQJe2BWUms9QOzdYlkA1WZ0uu7axPE3s+T5OOxfy7K9Q2gsLe72dcVSlffw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -1238,8 +1242,12 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@9.0.4: - resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} ms@2.1.2: @@ -1269,8 +1277,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: @@ -1343,8 +1351,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.3.2: - resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} hasBin: true @@ -1404,8 +1412,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.2: - resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true @@ -1481,8 +1489,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - synckit@0.8.8: - resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} + synckit@0.9.1: + resolution: {integrity: sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==} engines: {node: ^14.18.0 || >=16.0.0} test-exclude@6.0.0: @@ -1509,8 +1517,8 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-jest@29.1.5: - resolution: {integrity: sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==} + 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: @@ -1533,8 +1541,8 @@ packages: esbuild: optional: true - tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -1548,13 +1556,22 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + typescript-eslint@8.0.0: + resolution: {integrity: sha512-yQWBJutWL1PmpmDddIOl9/Mi6vZjqNCjqSGBMQ4vsc2Aiodk0SnbQQWPXbSy0HNuKCuGkw1+u4aQ2mO40TdhDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} update-browserslist-db@1.0.13: resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} @@ -1577,6 +1594,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'} @@ -1609,8 +1630,6 @@ packages: snapshots: - '@aashutoshrathi/word-wrap@1.2.6': {} - '@ampproject/remapping@2.2.1': dependencies: '@jridgewell/gen-mapping': 0.3.3 @@ -1636,7 +1655,7 @@ snapshots: '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 convert-source-map: 2.0.0 - debug: 4.3.5 + debug: 4.3.6 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1802,7 +1821,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - debug: 4.3.5 + debug: 4.3.6 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1815,19 +1834,17 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.0(eslint@9.5.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.9.1)': dependencies: - eslint: 9.5.0 + eslint: 9.9.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.10.0': {} + '@eslint-community/regexpp@4.11.0': {} - '@eslint-community/regexpp@4.10.1': {} - - '@eslint/config-array@0.16.0': + '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.4 + debug: 4.3.6 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -1835,10 +1852,10 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.4 - espree: 10.0.1 + debug: 4.3.6 + espree: 10.1.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 @@ -1846,7 +1863,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.5.0': {} + '@eslint/js@9.9.1': {} '@eslint/object-schema@2.1.4': {} @@ -1867,7 +1884,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1880,14 +1897,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 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@20.14.5) + jest-config: 29.7.0(@types/node@22.5.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1912,7 +1929,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1930,7 +1947,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.14.5 + '@types/node': 22.5.1 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1952,7 +1969,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 20.14.5 + '@types/node': 22.5.1 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2022,7 +2039,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.14.5 + '@types/node': 22.5.1 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2090,7 +2107,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 20.14.5 + '@types/node': 22.5.1 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2107,9 +2124,9 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 - '@types/node@20.14.5': + '@types/node@22.5.1': dependencies: - undici-types: 5.26.5 + undici-types: 6.19.8 '@types/stack-utils@2.0.3': {} @@ -2119,92 +2136,92 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5)': + '@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@eslint-community/regexpp': 4.10.1 - '@typescript-eslint/parser': 7.13.1(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/scope-manager': 7.13.1 - '@typescript-eslint/type-utils': 7.13.1(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.13.1 - eslint: 9.5.0 + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 8.0.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.0.0 + '@typescript-eslint/type-utils': 8.0.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.0.0 + eslint: 9.9.1 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.4.5) + ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.4.5)': + '@typescript-eslint/parser@8.0.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 7.13.1 - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.13.1 - debug: 4.3.5 - eslint: 9.5.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) + '@typescript-eslint/visitor-keys': 8.0.0 + debug: 4.3.6 + eslint: 9.9.1 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.13.1': + '@typescript-eslint/scope-manager@8.0.0': dependencies: - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/visitor-keys': 7.13.1 + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/visitor-keys': 8.0.0 - '@typescript-eslint/type-utils@7.13.1(eslint@9.5.0)(typescript@5.4.5)': + '@typescript-eslint/type-utils@8.0.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.4.5) - '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.4.5) - debug: 4.3.5 - eslint: 9.5.0 - ts-api-utils: 1.3.0(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.0(eslint@9.9.1)(typescript@5.5.4) + debug: 4.3.6 + ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: + - eslint - supports-color - '@typescript-eslint/types@7.13.1': {} + '@typescript-eslint/types@8.0.0': {} - '@typescript-eslint/typescript-estree@7.13.1(typescript@5.4.5)': + '@typescript-eslint/typescript-estree@8.0.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/visitor-keys': 7.13.1 - debug: 4.3.5 + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/visitor-keys': 8.0.0 + debug: 4.3.6 globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.2 - ts-api-utils: 1.3.0(typescript@5.4.5) + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.13.1(eslint@9.5.0)(typescript@5.4.5)': + '@typescript-eslint/utils@8.0.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) - '@typescript-eslint/scope-manager': 7.13.1 - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.4.5) - eslint: 9.5.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@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.9.1 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@7.13.1': + '@typescript-eslint/visitor-keys@8.0.0': dependencies: - '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/types': 8.0.0 eslint-visitor-keys: 3.4.3 - acorn-jsx@5.3.2(acorn@8.11.3): + acorn-jsx@5.3.2(acorn@8.12.1): dependencies: - acorn: 8.11.3 + acorn: 8.12.1 - acorn@8.11.3: {} + acorn@8.12.1: {} ajv@6.12.6: dependencies: @@ -2242,6 +2259,8 @@ snapshots: array-union@2.1.0: {} + async@3.2.6: {} + babel-jest@29.7.0(@babel/core@7.23.9): dependencies: '@babel/core': 7.23.9 @@ -2381,13 +2400,13 @@ snapshots: convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@20.14.5): + create-jest@29.7.0(@types/node@22.5.1): 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@20.14.5) + jest-config: 29.7.0(@types/node@22.5.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2402,11 +2421,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.3.4: - dependencies: - ms: 2.1.2 - - debug@4.3.5: + debug@4.3.6: dependencies: ms: 2.1.2 @@ -2424,7 +2439,9 @@ snapshots: dependencies: path-type: 4.0.0 - download-file-sync@1.0.4: {} + ejs@3.1.10: + dependencies: + jake: 10.9.2 electron-to-chromium@1.4.648: {} @@ -2444,14 +2461,14 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.1.3(eslint@9.5.0)(prettier@3.3.2): + eslint-plugin-prettier@5.2.1(eslint@9.9.1)(prettier@3.3.3): dependencies: - eslint: 9.5.0 - prettier: 3.3.2 + eslint: 9.9.1 + prettier: 3.3.3 prettier-linter-helpers: 1.0.0 - synckit: 0.8.8 + synckit: 0.9.1 - eslint-scope@8.0.1: + eslint-scope@8.0.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 @@ -2460,31 +2477,31 @@ snapshots: eslint-visitor-keys@4.0.0: {} - eslint@9.5.0: + eslint@9.9.1: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) - '@eslint-community/regexpp': 4.10.0 - '@eslint/config-array': 0.16.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@eslint-community/regexpp': 4.11.0 + '@eslint/config-array': 0.18.0 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.5.0 + '@eslint/js': 9.9.1 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.3.0 '@nodelib/fs.walk': 1.2.8 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.6 escape-string-regexp: 4.0.0 - eslint-scope: 8.0.1 + eslint-scope: 8.0.2 eslint-visitor-keys: 4.0.0 - espree: 10.0.1 - esquery: 1.5.0 + espree: 10.1.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 @@ -2493,21 +2510,21 @@ snapshots: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color - espree@10.0.1: + espree@10.1.0: dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) eslint-visitor-keys: 4.0.0 esprima@4.0.1: {} - esquery@1.5.0: + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -2569,6 +2586,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 @@ -2635,7 +2656,7 @@ snapshots: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.3.2 - ignore: 5.3.1 + ignore: 5.3.2 merge2: 1.4.1 slash: 3.0.0 @@ -2657,6 +2678,8 @@ snapshots: ignore@5.3.1: {} + ignore@5.3.2: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -2718,7 +2741,7 @@ snapshots: '@babel/parser': 7.23.9 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -2730,7 +2753,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.5 + debug: 4.3.6 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -2741,6 +2764,13 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + jest-changed-files@29.7.0: dependencies: execa: 5.1.1 @@ -2753,7 +2783,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -2773,16 +2803,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.14.5): + jest-cli@29.7.0(@types/node@22.5.1): 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@20.14.5) + create-jest: 29.7.0(@types/node@22.5.1) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.14.5) + jest-config: 29.7.0(@types/node@22.5.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2792,7 +2822,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.14.5): + jest-config@29.7.0(@types/node@22.5.1): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 @@ -2817,7 +2847,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.14.5 + '@types/node': 22.5.1 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2846,7 +2876,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2856,7 +2886,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.14.5 + '@types/node': 22.5.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2895,7 +2925,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2930,7 +2960,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2958,7 +2988,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -2997,14 +3027,14 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3023,7 +3053,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.5 + '@types/node': 22.5.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3032,17 +3062,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.14.5 + '@types/node': 22.5.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.14.5): + jest@29.7.0(@types/node@22.5.1): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.14.5) + jest-cli: 29.7.0(@types/node@22.5.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3078,7 +3108,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.19.4: {} + lemmy-js-client@0.19.5-alpha.1: {} leven@3.1.0: {} @@ -3107,7 +3137,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.2 + semver: 7.6.3 make-error@1.3.6: {} @@ -3135,7 +3165,11 @@ snapshots: dependencies: brace-expansion: 1.1.11 - minimatch@9.0.4: + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -3161,14 +3195,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: @@ -3225,7 +3259,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.3.2: {} + prettier@3.3.3: {} pretty-format@29.7.0: dependencies: @@ -3272,7 +3306,7 @@ snapshots: semver@6.3.1: {} - semver@7.6.2: {} + semver@7.6.3: {} shebang-command@2.0.0: dependencies: @@ -3334,10 +3368,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - synckit@0.8.8: + synckit@0.9.1: dependencies: '@pkgr/core': 0.1.1 - tslib: 2.6.2 + tslib: 2.6.3 test-exclude@6.0.0: dependencies: @@ -3355,21 +3389,22 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@1.3.0(typescript@5.4.5): + ts-api-utils@1.3.0(typescript@5.5.4): dependencies: - typescript: 5.4.5 + typescript: 5.5.4 - ts-jest@29.1.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@20.14.5))(typescript@5.4.5): + 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.5.1))(typescript@5.5.4): dependencies: bs-logger: 0.2.6 + ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.14.5) + jest: 29.7.0(@types/node@22.5.1) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.6.2 - typescript: 5.4.5 + semver: 7.6.3 + typescript: 5.5.4 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.23.9 @@ -3377,7 +3412,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.23.9) - tslib@2.6.2: {} + tslib@2.6.3: {} type-check@0.4.0: dependencies: @@ -3387,9 +3422,20 @@ snapshots: type-fest@0.21.3: {} - typescript@5.4.5: {} + typescript-eslint@8.0.0(eslint@9.9.1)(typescript@5.5.4): + dependencies: + '@typescript-eslint/eslint-plugin': 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/parser': 8.0.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.0(eslint@9.9.1)(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color - undici-types@5.26.5: {} + typescript@5.5.4: {} + + undici-types@6.19.8: {} update-browserslist-db@1.0.13(browserslist@4.22.3): dependencies: @@ -3415,6 +3461,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/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh index 31eb111c2..65c4827d9 100755 --- a/api_tests/prepare-drone-federation-test.sh +++ b/api_tests/prepare-drone-federation-test.sh @@ -15,7 +15,7 @@ export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queu # pictrs setup if [ ! -f "api_tests/pict-rs" ]; then - curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/pict-rs-linux-amd64" -o api_tests/pict-rs + curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o api_tests/pict-rs chmod +x api_tests/pict-rs fi ./api_tests/pict-rs \ diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index d172f7045..d8aa6cad6 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -1,5 +1,6 @@ jest.setTimeout(120000); +import { AddModToCommunity } from "lemmy-js-client/dist/types/AddModToCommunity"; import { CommunityView } from "lemmy-js-client/dist/types/CommunityView"; import { alpha, @@ -9,6 +10,7 @@ import { resolveCommunity, createCommunity, deleteCommunity, + delay, removeCommunity, getCommunity, followCommunity, @@ -533,3 +535,41 @@ test("Content in local-only community doesn't federate", async () => { Error("couldnt_find_object"), ); }); + +test("Remote mods can edit communities", async () => { + let communityRes = await createCommunity(alpha); + + let betaCommunity = await resolveCommunity( + beta, + communityRes.community_view.community.actor_id, + ); + if (!betaCommunity.community) { + throw "Missing beta community"; + } + let betaOnAlpha = await resolvePerson(alpha, "lemmy_beta@lemmy-beta:8551"); + + let form: AddModToCommunity = { + community_id: communityRes.community_view.community.id, + person_id: betaOnAlpha.person?.person.id as number, + added: true, + }; + alpha.addModToCommunity(form); + + let form2: EditCommunity = { + community_id: betaCommunity.community?.community.id as number, + description: "Example description", + }; + + await editCommunity(beta, form2); + // give alpha time to get and process the edit + await delay(1000); + + let alphaCommunity = await getCommunity( + alpha, + communityRes.community_view.community.id, + ); + + await expect(alphaCommunity.community_view.community.description).toBe( + "Example description", + ); +}); diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index 123982e85..ed96451a2 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -33,7 +33,6 @@ import { sampleImage, sampleSite, } from "./shared"; -const downloadFileSync = require("download-file-sync"); beforeAll(setupLogins); @@ -57,7 +56,8 @@ test("Upload image and delete it", async () => { expect(upload.delete_url).toBeDefined(); // ensure that image download is working. theres probably a better way to do this - const content = downloadFileSync(upload.url); + const response = await fetch(upload.url ?? ""); + const content = await response.text(); expect(content.length).toBeGreaterThan(0); // Ensure that it comes back with the list_media endpoint @@ -92,7 +92,8 @@ test("Upload image and delete it", async () => { expect(delete_).toBe(true); // ensure that image is deleted - const content2 = downloadFileSync(upload.url); + const response2 = await fetch(upload.url ?? ""); + const content2 = await response2.text(); expect(content2).toBe(""); // Ensure that it shows the image is deleted @@ -120,7 +121,8 @@ test("Purge user, uploaded image removed", async () => { expect(upload.delete_url).toBeDefined(); // ensure that image download is working. theres probably a better way to do this - const content = downloadFileSync(upload.url); + const response = await fetch(upload.url ?? ""); + const content = await response.text(); expect(content.length).toBeGreaterThan(0); // purge user @@ -132,7 +134,8 @@ test("Purge user, uploaded image removed", async () => { expect(delete_.success).toBe(true); // ensure that image is deleted - const content2 = downloadFileSync(upload.url); + const response2 = await fetch(upload.url ?? ""); + const content2 = await response2.text(); expect(content2).toBe(""); }); @@ -150,7 +153,8 @@ test("Purge post, linked image removed", async () => { expect(upload.delete_url).toBeDefined(); // ensure that image download is working. theres probably a better way to do this - const content = downloadFileSync(upload.url); + const response = await fetch(upload.url ?? ""); + const content = await response.text(); expect(content.length).toBeGreaterThan(0); let community = await resolveBetaCommunity(user); @@ -160,6 +164,7 @@ test("Purge post, linked image removed", async () => { upload.url, ); expect(post.post_view.post.url).toBe(upload.url); + expect(post.post_view.image_details).toBeDefined(); // purge post const purgeForm: PurgePost = { @@ -169,7 +174,8 @@ test("Purge post, linked image removed", async () => { expect(delete_.success).toBe(true); // ensure that image is deleted - const content2 = downloadFileSync(upload.url); + const response2 = await fetch(upload.url ?? ""); + const content2 = await response2.text(); expect(content2).toBe(""); }); @@ -184,6 +190,9 @@ test("Images in remote image post are proxied if setting enabled", async () => { const post = postRes.post_view.post; expect(post).toBeDefined(); + // Make sure it fetched the image details + expect(postRes.post_view.image_details).toBeDefined(); + // remote image gets proxied after upload expect( post.thumbnail_url?.startsWith( diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 75df80775..6b5c8d812 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -512,7 +512,7 @@ test("Enforce site ban federation for local user", async () => { } let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name); alphaUserHttp.setHeaders({ - Authorization: "Bearer " + newAlphaUserJwt.jwt ?? "", + Authorization: "Bearer " + newAlphaUserJwt.jwt, }); // alpha makes new post in beta community, it federates let postRes2 = await createPost(alphaUserHttp, betaCommunity!.community.id); diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 2ae3d9e21..3ca37dac4 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -197,7 +197,7 @@ export async function setupLogins() { // (because last_successful_id is set to current id when federation to an instance is first started) // only needed the first time so do in this try await delay(10_000); - } catch (_) { + } catch { console.log("Communities already exist"); } } @@ -899,7 +899,6 @@ export async function deleteAllImages(api: LemmyHttp) { const imagesRes = await api.listAllMedia({ limit: imageFetchLimit, }); - imagesRes.images; Promise.all( imagesRes.images .map(image => { diff --git a/cliff.toml b/cliff.toml index d8975a171..b5b8c3f16 100644 --- a/cliff.toml +++ b/cliff.toml @@ -26,6 +26,7 @@ body = """ {%- endif %} {%- endfor -%} +{%- if github -%} {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} {% raw %}\n{% endraw -%} ## New Contributors @@ -36,6 +37,7 @@ body = """ [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ {%- endif %} {%- endfor -%} +{%- endif -%} {% if version %} {% if previous.version %} @@ -70,6 +72,7 @@ commit_preprocessors = [ # remove issue numbers from commits { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, ] +commit_parsers = [{ field = "author.name", pattern = "renovate", skip = true }] # protect breaking changes from being skipped due to matching a skipping commit_parser protect_breaking_commits = false # filter out the commits that are not matched by commit parsers diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index b98b15d62..87879f6cd 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -35,11 +35,12 @@ chrono = { workspace = true } url = { workspace = true } hound = "3.5.1" sitemap-rs = "0.2.1" -totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] } -actix-web-httpauth = "0.8.1" +totp-rs = { version = "5.6.0", features = ["gen_secret", "otpauth"] } +actix-web-httpauth = "0.8.2" [dev-dependencies] serial_test = { workspace = true } tokio = { workspace = true } elementtree = "1.2.3" pretty_assertions = { workspace = true } +lemmy_api_crud = { workspace = true } diff --git a/crates/api/src/comment/distinguish.rs b/crates/api/src/comment/distinguish.rs index dfd850e89..0683af9a4 100644 --- a/crates/api/src/comment/distinguish.rs +++ b/crates/api/src/comment/distinguish.rs @@ -17,9 +17,13 @@ pub async fn distinguish_comment( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + data.comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, @@ -54,7 +58,7 @@ pub async fn distinguish_comment( let comment_view = CommentView::read( &mut context.pool(), data.comment_id, - Some(local_user_view.person.id), + Some(&local_user_view.local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index d0aa4a6c2..b8a1c6f76 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -35,9 +35,13 @@ pub async fn like_comment( check_bot_account(&local_user_view.person)?; let comment_id = data.comment_id; - let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, diff --git a/crates/api/src/comment/list_comment_likes.rs b/crates/api/src/comment/list_comment_likes.rs index 8c2c9dd32..4b2e1c8b3 100644 --- a/crates/api/src/comment/list_comment_likes.rs +++ b/crates/api/src/comment/list_comment_likes.rs @@ -17,7 +17,7 @@ pub async fn list_comment_likes( let comment_view = CommentView::read( &mut context.pool(), data.comment_id, - Some(local_user_view.person.id), + Some(&local_user_view.local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs index f9d649e48..67c2db331 100644 --- a/crates/api/src/comment/save.rs +++ b/crates/api/src/comment/save.rs @@ -32,10 +32,13 @@ pub async fn save_comment( } let comment_id = data.comment_id; - let person_id = local_user_view.person.id; - let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; Ok(Json(CommentResponse { comment_view, diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs index c008d1df2..a269df07f 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/comment_report/create.rs @@ -35,9 +35,13 @@ pub async fn create_comment_report( let person_id = local_user_view.person.id; let comment_id = data.comment_id; - let comment_view = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 5245f592e..8d8826cd2 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, + local_user::LocalUser, moderator::{ModAddCommunity, ModAddCommunityForm}, }, traits::{Crud, Joinable}, @@ -33,6 +34,18 @@ pub async fn add_mod_to_community( &mut context.pool(), ) .await?; + + // If its a mod removal, also check that you're a higher mod. + if !data.added { + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + 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)?; diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 877d9464f..8e527d2ac 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -14,6 +14,7 @@ use lemmy_db_schema::{ CommunityPersonBan, CommunityPersonBanForm, }, + local_user::LocalUser, moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, }, traits::{Bannable, Crud, Followable}, @@ -44,6 +45,14 @@ pub async fn ban_from_community( ) .await?; + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + data.community_id, + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + if let Some(reason) = &data.reason { is_valid_body_field(reason, false)?; } diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs index 449addf32..ad31548ea 100644 --- a/crates/api/src/community/block.rs +++ b/crates/api/src/community/block.rs @@ -50,10 +50,14 @@ pub async fn block_community( .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?; } - let community_view = - CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community_view = CommunityView::read( + &mut context.pool(), + community_id, + Some(&local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; ActivityChannel::submit_activity( SendActivityData::FollowCommunity( diff --git a/crates/api/src/community/follow.rs b/crates/api/src/community/follow.rs index 853cfde14..2236fa5bc 100644 --- a/crates/api/src/community/follow.rs +++ b/crates/api/src/community/follow.rs @@ -62,11 +62,14 @@ pub async fn follow_community( } let community_id = data.community_id; - let person_id = local_user_view.person.id; - let community_view = - CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community_view = CommunityView::read( + &mut context.pool(), + community_id, + Some(&local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index 5f3a6032e..a32c069b1 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -76,11 +76,14 @@ pub async fn transfer_community( ModTransferCommunity::create(&mut context.pool(), &form).await?; let community_id = data.community_id; - let person_id = local_user_view.person.id; - let community_view = - CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community_view = CommunityView::read( + &mut context.pool(), + community_id, + Some(&local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let community_id = data.community_id; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id) diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs index 7db2e5653..44b36fe66 100644 --- a/crates/api/src/local_user/add_admin.rs +++ b/crates/api/src/local_user/add_admin.rs @@ -24,6 +24,16 @@ pub async fn add_admin( // Make sure user is an admin is_admin(&local_user_view)?; + // If its an admin removal, also check that you're a higher admin + if !data.added { + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + } + // Make sure that the person_id added is local let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id) .await? diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 49cd6893a..58392cefd 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + local_user::LocalUser, login_token::LoginToken, moderator::{ModBan, ModBanForm}, person::{Person, PersonUpdateForm}, @@ -31,6 +32,14 @@ pub async fn ban_from_site( // Make sure user is an admin is_admin(&local_user_view)?; + // Also make sure you're a higher admin than the target + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + if let Some(reason) = &data.reason { is_valid_body_field(reason, false)?; } diff --git a/crates/api/src/local_user/verify_email.rs b/crates/api/src/local_user/verify_email.rs index 5f38ffc12..5b895ec7e 100644 --- a/crates/api/src/local_user/verify_email.rs +++ b/crates/api/src/local_user/verify_email.rs @@ -5,12 +5,9 @@ use lemmy_api_common::{ utils::send_new_applicant_email_to_admins, SuccessResponse, }; -use lemmy_db_schema::{ - source::{ - email_verification::EmailVerification, - local_user::{LocalUser, LocalUserUpdateForm}, - }, - RegistrationMode, +use lemmy_db_schema::source::{ + email_verification::EmailVerification, + local_user::{LocalUser, LocalUserUpdateForm}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -41,9 +38,7 @@ pub async fn verify_email( EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?; // send out notification about registration application to admins if enabled - if site_view.local_site.registration_mode == RegistrationMode::RequireApplication - && site_view.local_site.application_email_admins - { + if site_view.local_site.application_email_admins { let local_user = LocalUserView::read(&mut context.pool(), local_user_id) .await? .ok_or(LemmyErrorType::CouldntFindPerson)?; diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index 40cbf6794..ec99a3345 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -72,11 +72,5 @@ pub async fn feature_post( ) .await?; - build_post_response( - &context, - orig_post.community_id, - &local_user_view.person, - post_id, - ) - .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 0669408aa..e469b51c7 100644 --- a/crates/api/src/post/get_link_metadata.rs +++ b/crates/api/src/post/get_link_metadata.rs @@ -4,6 +4,7 @@ use lemmy_api_common::{ post::{GetSiteMetadata, GetSiteMetadataResponse}, request::fetch_link_metadata, }; +use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ error::{LemmyErrorExt, LemmyResult}, LemmyErrorType, @@ -14,6 +15,8 @@ use url::Url; pub async fn get_link_metadata( data: Query, context: Data, + // Require an account for this API + _local_user_view: LocalUserView, ) -> LemmyResult> { let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?; let metadata = fetch_link_metadata(&url, &context).await?; diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index fccd9f8df..e6903fb3c 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -85,11 +85,5 @@ pub async fn like_post( ) .await?; - build_post_response( - context.deref(), - post.community_id, - &local_user_view.person, - post_id, - ) - .await + build_post_response(context.deref(), post.community_id, local_user_view, post_id).await } diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index 05db8ebbb..36f9c2a33 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -63,11 +63,5 @@ pub async fn lock_post( ) .await?; - build_post_response( - &context, - orig_post.community_id, - &local_user_view.person, - 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 96dd85579..85dfc11e3 100644 --- a/crates/api/src/post/save.rs +++ b/crates/api/src/post/save.rs @@ -34,9 +34,14 @@ pub async fn save_post( let post_id = data.post_id; let person_id = local_user_view.person.id; - let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post_view = PostView::read( + &mut context.pool(), + post_id, + Some(&local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?; diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index 70d95e160..9f90aff99 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -10,6 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ comment::Comment, + local_user::LocalUser, moderator::{AdminPurgeComment, AdminPurgeCommentForm}, }, traits::Crud, @@ -29,9 +30,21 @@ pub async fn purge_comment( let comment_id = data.comment_id; // Read the comment to get the post_id and community - let comment_view = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; + + // Also check that you're a higher admin + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![comment_view.creator.id], + ) + .await?; let post_id = comment_view.comment.post_id; diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index 14b250681..59eded6ad 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -9,13 +9,16 @@ use lemmy_api_common::{ SuccessResponse, }; use lemmy_db_schema::{ + newtypes::PersonId, source::{ community::Community, + local_user::LocalUser, moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm}, }, traits::Crud, }; use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] @@ -32,6 +35,21 @@ pub async fn purge_community( .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?; + // Also check that you're a higher admin than all the mods + let community_mod_person_ids = + CommunityModeratorView::for_community(&mut context.pool(), community.id) + .await? + .iter() + .map(|cmv| cmv.moderator.id) + .collect::>(); + + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + community_mod_person_ids, + ) + .await?; + if let Some(banner) = &community.banner { purge_image_from_pictrs(banner, &context).await.ok(); } diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 1b38752c7..dc824b163 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -10,6 +10,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + local_user::LocalUser, moderator::{AdminPurgePerson, AdminPurgePersonForm}, person::{Person, PersonUpdateForm}, }, @@ -27,9 +28,18 @@ pub async fn purge_person( // Only let admin purge an item is_admin(&local_user_view)?; + // Also check that you're a higher admin + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + let person = Person::read(&mut context.pool(), data.person_id) .await? .ok_or(LemmyErrorType::CouldntFindPerson)?; + ban_nonlocal_user_from_local_communities( &local_user_view, &person, diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index 75cd021d1..6e512312f 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -10,6 +10,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + local_user::LocalUser, moderator::{AdminPurgePost, AdminPurgePostForm}, post::Post, }, @@ -32,6 +33,14 @@ pub async fn purge_post( .await? .ok_or(LemmyErrorType::CouldntFindPost)?; + // Also check that you're a higher admin + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![post.creator_id], + ) + .await?; + // Purge image if let Some(url) = &post.url { purge_image_from_pictrs(url, &context).await.ok(); diff --git a/crates/api/src/site/registration_applications/approve.rs b/crates/api/src/site/registration_applications/approve.rs index 823af54c4..dcde78117 100644 --- a/crates/api/src/site/registration_applications/approve.rs +++ b/crates/api/src/site/registration_applications/approve.rs @@ -1,4 +1,5 @@ -use actix_web::web::{Data, Json}; +use activitypub_federation::config::Data; +use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, site::{ApproveRegistrationApplication, RegistrationApplicationResponse}, @@ -10,10 +11,13 @@ use lemmy_db_schema::{ registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm}, }, traits::Crud, - utils::diesel_string_update, + utils::{diesel_string_update, get_conn}, }; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, +}; pub async fn approve_registration_application( data: Json, @@ -25,34 +29,46 @@ pub async fn approve_registration_application( // Only let admins do this is_admin(&local_user_view)?; - // Update the registration with reason, admin_id - let deny_reason = diesel_string_update(data.deny_reason.as_deref()); - let app_form = RegistrationApplicationUpdateForm { - admin_id: Some(Some(local_user_view.person.id)), - deny_reason, - }; + let pool = &mut context.pool(); + let conn = &mut get_conn(pool).await?; + let tx_data = data.clone(); + let approved_user_id = conn + .build_transaction() + .run(|conn| { + Box::pin(async move { + // Update the registration with reason, admin_id + let deny_reason = diesel_string_update(tx_data.deny_reason.as_deref()); + let app_form = RegistrationApplicationUpdateForm { + admin_id: Some(Some(local_user_view.person.id)), + deny_reason, + }; - let registration_application = - RegistrationApplication::update(&mut context.pool(), app_id, &app_form).await?; + let registration_application = + RegistrationApplication::update(&mut conn.into(), app_id, &app_form).await?; - // Update the local_user row - let local_user_form = LocalUserUpdateForm { - accepted_application: Some(data.approve), - ..Default::default() - }; + // Update the local_user row + let local_user_form = LocalUserUpdateForm { + accepted_application: Some(tx_data.approve), + ..Default::default() + }; - let approved_user_id = registration_application.local_user_id; - LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?; + let approved_user_id = registration_application.local_user_id; + LocalUser::update(&mut conn.into(), approved_user_id, &local_user_form).await?; + + Ok::<_, LemmyError>(approved_user_id) + }) as _ + }) + .await?; if data.approve { let approved_local_user_view = LocalUserView::read(&mut context.pool(), approved_user_id) .await? .ok_or(LemmyErrorType::CouldntFindLocalUser)?; - 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?; } - } + }; // Read the view let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id) diff --git a/crates/api/src/site/registration_applications/get.rs b/crates/api/src/site/registration_applications/get.rs new file mode 100644 index 000000000..2d5d6bf5b --- /dev/null +++ b/crates/api/src/site/registration_applications/get.rs @@ -0,0 +1,28 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + site::{GetRegistrationApplication, RegistrationApplicationResponse}, + utils::is_admin, +}; +use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + +/// Lists registration applications, filterable by undenied only. +pub async fn get_registration_application( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + // Read the view + let registration_application = + RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; + + Ok(Json(RegistrationApplicationResponse { + registration_application, + })) +} diff --git a/crates/api/src/site/registration_applications/list.rs b/crates/api/src/site/registration_applications/list.rs index df86b11d5..877e83796 100644 --- a/crates/api/src/site/registration_applications/list.rs +++ b/crates/api/src/site/registration_applications/list.rs @@ -1,4 +1,5 @@ -use actix_web::web::{Data, Json, Query}; +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; use lemmy_api_common::{ context::LemmyContext, site::{ListRegistrationApplications, ListRegistrationApplicationsResponse}, diff --git a/crates/api/src/site/registration_applications/mod.rs b/crates/api/src/site/registration_applications/mod.rs index e5082615a..c9a63cdef 100644 --- a/crates/api/src/site/registration_applications/mod.rs +++ b/crates/api/src/site/registration_applications/mod.rs @@ -1,3 +1,6 @@ pub mod approve; +pub mod get; pub mod list; +#[cfg(test)] +mod tests; pub mod unread_count; diff --git a/crates/api/src/site/registration_applications/tests.rs b/crates/api/src/site/registration_applications/tests.rs new file mode 100644 index 000000000..062fa550f --- /dev/null +++ b/crates/api/src/site/registration_applications/tests.rs @@ -0,0 +1,428 @@ +use crate::site::registration_applications::{ + approve::approve_registration_application, + list::list_registration_applications, + unread_count::get_unread_registration_application_count, +}; +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + site::{ + ApproveRegistrationApplication, + EditSite, + GetUnreadRegistrationApplicationCountResponse, + ListRegistrationApplicationsResponse, + }, +}; +use lemmy_api_crud::site::update::update_site; +use lemmy_db_schema::{ + newtypes::InstanceId, + source::{ + instance::Instance, + local_site::{LocalSite, LocalSiteInsertForm}, + local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm}, + local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, + person::{Person, PersonInsertForm}, + registration_application::{RegistrationApplication, RegistrationApplicationInsertForm}, + site::{Site, SiteInsertForm}, + }, + traits::Crud, + utils::DbPool, + RegistrationMode, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::{error::LemmyResult, LemmyErrorType, 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 admin_person = Person::create( + pool, + &PersonInsertForm::test_form(inserted_instance.id, "admin"), + ) + .await?; + LocalUser::create( + pool, + &LocalUserInsertForm::test_form_admin(admin_person.id), + vec![], + ) + .await?; + + let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id) + .await? + .unwrap(); + + 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(); + + // 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(); + + // 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(); + + Ok((inserted_instance, admin_local_user_view)) +} + +async fn signup( + pool: &mut DbPool<'_>, + instance_id: InstanceId, + name: &str, + email: Option<&str>, +) -> LemmyResult<(LocalUser, RegistrationApplication)> { + let person_insert_form = PersonInsertForm::test_form(instance_id, name); + let person = Person::create(pool, &person_insert_form).await?; + + let local_user_insert_form = match email { + Some(email) => LocalUserInsertForm { + email: Some(email.to_string()), + email_verified: Some(false), + ..LocalUserInsertForm::test_form(person.id) + }, + None => LocalUserInsertForm::test_form(person.id), + }; + + let local_user = LocalUser::create(pool, &local_user_insert_form, vec![]).await?; + + let application_insert_form = RegistrationApplicationInsertForm { + local_user_id: local_user.id, + answer: "x".to_string(), + }; + let application = RegistrationApplication::create(pool, &application_insert_form).await?; + + Ok((local_user, application)) +} + +#[allow(clippy::unwrap_used)] +async fn get_application_statuses( + context: &Data, + admin: LocalUserView, +) -> LemmyResult<( + Json, + Json, + Json, +)> { + let application_count = + 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(), + context.reset_request_count(), + admin.clone(), + ) + .await?; + + let all_applications = list_registration_applications( + Query::from_query("unread_only=false").unwrap(), + context.reset_request_count(), + admin, + ) + .await?; + + Ok((application_count, unread_applications, all_applications)) +} + +#[allow(clippy::indexing_slicing)] +#[allow(clippy::unwrap_used)] +#[tokio::test] +#[serial] +async fn test_application_approval() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); + + let (instance, admin_local_user_view) = create_test_site(&context).await?; + + // Non-unread counts unfortunately are duplicated due to different types (i64 vs usize) + let mut expected_total_applications = 0; + let mut expected_unread_applications = 0u8; + + let (local_user_with_email, app_with_email) = + signup(pool, instance.id, "user_w_email", Some("lemmy@localhost")).await?; + + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // When email verification is required and the email is not verified the application should not + // be visible to admins + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + + LocalUser::update( + pool, + local_user_with_email.id, + &LocalUserUpdateForm { + email_verified: Some(true), + ..Default::default() + }, + ) + .await?; + + expected_total_applications += 1; + expected_unread_applications += 1; + + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // When email verification is required and the email is verified the application should be + // visible to admins + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert!( + !unread_applications.registration_applications[0] + .creator_local_user + .accepted_application + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + + let approval = approve_registration_application( + Json(ApproveRegistrationApplication { + id: app_with_email.id, + approve: true, + deny_reason: None, + }), + context.reset_request_count(), + admin_local_user_view.clone(), + ) + .await; + // Approval should be processed up until email sending is attempted + assert!(approval.is_err_and(|e| e.error_type == LemmyErrorType::NoEmailSetup)); + + expected_unread_applications -= 1; + + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // When the application is approved it should only be returned for unread queries + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + assert!( + all_applications.registration_applications[0] + .creator_local_user + .accepted_application + ); + + let (_local_user, app_with_email_2) = signup( + pool, + instance.id, + "user_w_email_2", + Some("lemmy2@localhost"), + ) + .await?; + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // Email not verified, so application still not visible + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + + update_site( + Json(EditSite { + require_email_verification: Some(false), + ..Default::default() + }), + context.reset_request_count(), + admin_local_user_view.clone(), + ) + .await?; + + // TODO: There is probably a better way to ensure cache invalidation + tokio::time::sleep(CACHE_DURATION_API).await; + + expected_total_applications += 1; + expected_unread_applications += 1; + + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // After disabling email verification the application should now be visible + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + + approve_registration_application( + Json(ApproveRegistrationApplication { + id: app_with_email_2.id, + approve: false, + deny_reason: None, + }), + context.reset_request_count(), + admin_local_user_view.clone(), + ) + .await?; + + expected_unread_applications -= 1; + + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // Denied applications should not be marked as unread + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + + signup(pool, instance.id, "user_wo_email", None).await?; + + expected_total_applications += 1; + expected_unread_applications += 1; + + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // New user without email should immediately be visible + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + + signup(pool, instance.id, "user_w_email_3", None).await?; + + expected_total_applications += 1; + expected_unread_applications += 1; + + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // New user with email should immediately be visible + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + + update_site( + Json(EditSite { + registration_mode: Some(RegistrationMode::Open), + ..Default::default() + }), + context.reset_request_count(), + admin_local_user_view.clone(), + ) + .await?; + + // TODO: There is probably a better way to ensure cache invalidation + tokio::time::sleep(CACHE_DURATION_API).await; + + let (application_count, unread_applications, all_applications) = + get_application_statuses(&context, admin_local_user_view.clone()).await?; + + // TODO: At this time applications do not get approved when switching to open registration, so the + // numbers will not change. See https://github.com/LemmyNet/lemmy/issues/4969 + // expected_application_count = 0; + // expected_unread_applications_len = 0; + + // When applications are not required all previous applications should become approved but still + // visible + assert_eq!( + application_count.registration_applications, + i64::from(expected_unread_applications), + ); + assert_eq!( + unread_applications.registration_applications.len(), + usize::from(expected_unread_applications), + ); + assert_eq!( + all_applications.registration_applications.len(), + expected_total_applications, + ); + + LocalSite::delete(pool).await?; + // Instance deletion cascades cleanup of all created persons + Instance::delete(pool, instance.id).await?; + + Ok(()) +} diff --git a/crates/api/src/site/registration_applications/unread_count.rs b/crates/api/src/site/registration_applications/unread_count.rs index a12ecb1d3..5cc391ed0 100644 --- a/crates/api/src/site/registration_applications/unread_count.rs +++ b/crates/api/src/site/registration_applications/unread_count.rs @@ -1,4 +1,5 @@ -use actix_web::web::{Data, Json}; +use activitypub_federation::config::Data; +use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, site::GetUnreadRegistrationApplicationCountResponse, diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 25bc02893..4eabfe5f9 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -34,7 +34,6 @@ full = [ "reqwest", "actix-web", "futures", - "once_cell", "jsonwebtoken", "mime", ] @@ -61,7 +60,6 @@ reqwest = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true } moka.workspace = true anyhow.workspace = true -once_cell = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } enum-map = { workspace = true } urlencoding = { workspace = true } diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs index 85cf065eb..8f140f2fe 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -36,8 +36,8 @@ pub async fn build_comment_response( local_user_view: Option, recipient_ids: Vec, ) -> LemmyResult { - let person_id = local_user_view.map(|l| l.person.id); - let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id) + 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)?; Ok(CommentResponse { @@ -54,11 +54,11 @@ pub async fn build_community_response( let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id) .await .is_ok(); - let person_id = local_user_view.person.id; + let local_user = local_user_view.local_user; let community_view = CommunityView::read( &mut context.pool(), community_id, - Some(person_id), + Some(&local_user), is_mod_or_admin, ) .await? @@ -74,16 +74,17 @@ pub async fn build_community_response( pub async fn build_post_response( context: &LemmyContext, community_id: CommunityId, - person: &Person, + local_user_view: LocalUserView, post_id: PostId, ) -> LemmyResult> { - let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), person, community_id) + let local_user = local_user_view.local_user; + let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id) .await .is_ok(); let post_view = PostView::read( &mut context.pool(), post_id, - Some(person.id), + Some(&local_user), is_mod_or_admin, ) .await? @@ -99,14 +100,20 @@ pub async fn send_local_notifs( person: &Person, do_send_email: bool, context: &LemmyContext, + local_user_view: Option<&LocalUserView>, ) -> LemmyResult> { let mut recipient_ids = Vec::new(); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); + // let person = my_local_user.person; // Read the comment view to get extra info - let comment_view = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + local_user_view.map(|view| &view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; let comment = comment_view.comment; let post = comment_view.post; let community = comment_view.community; diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 6c17d4e6a..905394785 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -116,10 +116,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("123456".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) .await diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 3d1bc4078..74369173b 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -79,6 +79,10 @@ pub struct GetPosts { pub liked_only: Option, pub disliked_only: Option, pub show_hidden: Option, + /// If true, then show the read posts (even if your user setting is to hide them) + pub show_read: Option, + /// If true, then show the nsfw posts (even if your user setting is to hide them) + pub show_nsfw: Option, pub page_cursor: Option, } diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index 8a423ff7c..de6ba4f39 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -8,10 +8,11 @@ use crate::{ use activitypub_federation::config::Data; use chrono::{DateTime, Utc}; use encoding_rs::{Encoding, UTF_8}; +use futures::StreamExt; use lemmy_db_schema::{ newtypes::DbUrl, source::{ - images::{LocalImage, LocalImageForm}, + images::{ImageDetailsForm, LocalImage, LocalImageForm}, local_site::LocalSite, post::{Post, PostUpdateForm}, }, @@ -23,7 +24,12 @@ use lemmy_utils::{ VERSION, }; use mime::Mime; -use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder}; +use reqwest::{ + header::{CONTENT_TYPE, RANGE}, + Client, + ClientBuilder, + Response, +}; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; use tracing::info; @@ -44,7 +50,17 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder { #[tracing::instrument(skip_all)] pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult { info!("Fetching site metadata for url: {}", url); - let response = context.client().get(url.as_str()).send().await?; + // We only fetch the first 64kB of data in order to not waste bandwidth especially for large + // binary files + let bytes_to_fetch = 64 * 1024; + let response = context + .client() + .get(url.as_str()) + // we only need the first chunk of data. Note that we do not check for Accept-Range so the + // server may ignore this and still respond with the full response + .header(RANGE, format!("bytes=0-{}", bytes_to_fetch - 1)) /* -1 because inclusive */ + .send() + .await?; let content_type: Option = response .headers() @@ -52,19 +68,57 @@ pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResu .and_then(|h| h.to_str().ok()) .and_then(|h| h.parse().ok()); - // Can't use .text() here, because it only checks the content header, not the actual bytes - // https://github.com/LemmyNet/lemmy/issues/1964 - let html_bytes = response.bytes().await.map_err(LemmyError::from)?.to_vec(); + let opengraph_data = { + // if the content type is not text/html, we don't need to parse it + let is_html = content_type + .as_ref() + .map(|c| { + (c.type_() == mime::TEXT && c.subtype() == mime::HTML) + || + // application/xhtml+xml is a subset of HTML + (c.type_() == mime::APPLICATION && c.subtype() == "xhtml") + }) + .unwrap_or(false); + if !is_html { + Default::default() + } else { + // Can't use .text() here, because it only checks the content header, not the actual bytes + // https://github.com/LemmyNet/lemmy/issues/1964 + // So we want to do deep inspection of the actually returned bytes but need to be careful not + // spend too much time parsing binary data as HTML - let opengraph_data = extract_opengraph_data(&html_bytes, url) - .map_err(|e| info!("{e}")) - .unwrap_or_default(); + // only take first bytes regardless of how many bytes the server returns + let html_bytes = collect_bytes_until_limit(response, bytes_to_fetch).await?; + extract_opengraph_data(&html_bytes, url) + .map_err(|e| info!("{e}")) + .unwrap_or_default() + } + }; Ok(LinkMetadata { opengraph_data, content_type: content_type.map(|c| c.to_string()), }) } +async fn collect_bytes_until_limit( + response: Response, + requested_bytes: usize, +) -> Result, LemmyError> { + let mut stream = response.bytes_stream(); + let mut bytes = Vec::with_capacity(requested_bytes); + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(LemmyError::from)?; + // we may go over the requested size here but the important part is we don't keep aggregating + // more chunks than needed + bytes.extend_from_slice(&chunk); + if bytes.len() >= requested_bytes { + bytes.truncate(requested_bytes); + break; + } + } + Ok(bytes) +} + /// Generates and saves a post thumbnail and metadata. /// /// Takes a callback to generate a send activity task, so that post can be federated with metadata. @@ -209,6 +263,19 @@ pub struct PictrsFileDetails { pub created_at: DateTime, } +impl PictrsFileDetails { + /// Builds the image form. This should always use the thumbnail_url, + /// Because the post_view joins to it + pub fn build_image_details_form(&self, thumbnail_url: &Url) -> ImageDetailsForm { + ImageDetailsForm { + link: thumbnail_url.clone().into(), + width: self.width.into(), + height: self.height.into(), + content_type: self.content_type.clone(), + } + } +} + #[derive(Deserialize, Serialize, Debug)] struct PictrsPurgeResponse { msg: String, @@ -316,11 +383,52 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L let protocol_and_hostname = context.settings().get_protocol_and_hostname(); let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?; - LocalImage::create(&mut context.pool(), &form).await?; + // Also store the details for the image + let details_form = image.details.build_image_details_form(&thumbnail_url); + LocalImage::create(&mut context.pool(), &form, &details_form).await?; Ok(thumbnail_url) } +/// Fetches the image details for pictrs proxied images +/// +/// We don't need to check for image mode, as that's already been done +#[tracing::instrument(skip_all)] +pub async fn fetch_pictrs_proxied_image_details( + image_url: &Url, + context: &LemmyContext, +) -> LemmyResult { + let pictrs_url = context.settings().pictrs_config()?.url; + let encoded_image_url = encode(image_url.as_str()); + + // Pictrs needs you to fetch the proxied image before you can fetch the details + let proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}"); + + let res = context + .client() + .get(&proxy_url) + .timeout(REQWEST_TIMEOUT) + .send() + .await? + .status(); + if !res.is_success() { + Err(LemmyErrorType::NotAnImageType)? + } + + let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}"); + + let res = context + .client() + .get(&details_url) + .timeout(REQWEST_TIMEOUT) + .send() + .await? + .json() + .await?; + + Ok(res) +} + // TODO: get rid of this by reading content type from db #[tracing::instrument(skip_all)] async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> LemmyResult<()> { diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index a8b75d4b5..02518ca33 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::PrivateMessageView; use lemmy_utils::error::LemmyResult; -use once_cell::sync::{Lazy, OnceCell}; +use std::sync::{LazyLock, OnceLock}; use tokio::{ sync::{ mpsc, @@ -28,7 +28,7 @@ type MatchOutgoingActivitiesBoxed = Box fn(SendActivityData, &'a Data) -> BoxFuture<'a, LemmyResult<()>>>; /// This static is necessary so that the api_common crates don't need to depend on lemmy_apub -pub static MATCH_OUTGOING_ACTIVITIES: OnceCell = OnceCell::new(); +pub static MATCH_OUTGOING_ACTIVITIES: OnceLock = OnceLock::new(); #[derive(Debug)] pub enum SendActivityData { @@ -101,7 +101,7 @@ pub enum SendActivityData { // TODO: instead of static, move this into LemmyContext. make sure that stopping the process with // ctrl+c still works. -static ACTIVITY_CHANNEL: Lazy = Lazy::new(|| { +static ACTIVITY_CHANNEL: LazyLock = LazyLock::new(|| { let (sender, receiver) = mpsc::unbounded_channel(); let weak_sender = sender.downgrade(); ActivityChannel { diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 77bfcd8ee..3850de1c6 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -1,7 +1,15 @@ use crate::federate_retry_sleep_duration; use chrono::{DateTime, Utc}; use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, InstanceId, LanguageId, PersonId, PostId}, + newtypes::{ + CommentId, + CommunityId, + InstanceId, + LanguageId, + PersonId, + PostId, + RegistrationApplicationId, + }, source::{ federation_queue_state::FederationQueueState, instance::Instance, @@ -440,13 +448,22 @@ pub struct ListRegistrationApplicationsResponse { pub registration_applications: 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))] +/// Gets a registration application for a person +pub struct GetRegistrationApplication { + 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))] /// Approves a registration application. pub struct ApproveRegistrationApplication { - pub id: i32, + pub id: RegistrationApplicationId, pub approve: bool, pub deny_reason: Option, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index ba5756998..0b8e56273 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -1,6 +1,10 @@ use crate::{ context::LemmyContext, - request::{delete_image_from_pictrs, purge_image_from_pictrs}, + request::{ + delete_image_from_pictrs, + fetch_pictrs_proxied_image_details, + purge_image_from_pictrs, + }, site::{FederatedInstances, InstanceWithFederationState}, }; use chrono::{DateTime, Days, Local, TimeZone, Utc}; @@ -49,10 +53,9 @@ use lemmy_utils::{ CACHE_DURATION_FEDERATION, }; use moka::future::Cache; -use once_cell::sync::Lazy; use regex::{escape, Regex, RegexSet}; use rosetta_i18n::{Language, LanguageId}; -use std::collections::HashSet; +use std::{collections::HashSet, sync::LazyLock}; use tracing::warn; use url::{ParseError, Url}; use urlencoding::encode; @@ -542,7 +545,7 @@ pub fn local_site_opt_to_sensitive(local_site: &Option) -> bool { } pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult { - static URL_BLOCKLIST: Lazy> = Lazy::new(|| { + static URL_BLOCKLIST: LazyLock> = LazyLock::new(|| { Cache::builder() .max_capacity(1) .time_to_live(CACHE_DURATION_FEDERATION) @@ -949,7 +952,18 @@ pub async fn process_markdown( if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages { let (text, links) = markdown_rewrite_image_links(text); - RemoteImage::create(&mut context.pool(), links).await?; + + // Create images and image detail rows + for link in links { + // Insert image details for the remote image + let details_res = fetch_pictrs_proxied_image_details(&link, context).await; + if let Ok(details) = details_res { + 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?; + } + } Ok(text) } else { Ok(text) @@ -984,8 +998,14 @@ async fn proxy_image_link_internal( Ok(link.into()) } else if image_mode == PictrsImageMode::ProxyAllImages { 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?; + }; - RemoteImage::create(&mut context.pool(), vec![link]).await?; Ok(proxied.into()) } else { Ok(link.into()) @@ -1123,10 +1143,13 @@ mod tests { "https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png", proxied.as_str() ); + + // This fails, because the details can't be fetched without pictrs running, + // And a remote image won't be inserted. assert!( RemoteImage::validate(&mut context.pool(), remote_image.into()) .await - .is_ok() + .is_err() ); } } diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index af50c5648..6055f9ef0 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -26,7 +26,6 @@ url = { workspace = true } futures.workspace = true uuid = { workspace = true } moka.workspace = true -once_cell.workspace = true anyhow.workspace = true webmention = "0.5.0" accept-language = "3.1.0" diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 636f83392..49de9d5de 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -8,20 +8,18 @@ use lemmy_api_common::{ utils::{ check_community_user_action, check_post_deleted_or_removed, - generate_local_apub_endpoint, get_url_blocklist, is_mod_or_admin, local_site_to_slur_regex, process_markdown, update_read_comments, - EndpointType, }, }; use lemmy_db_schema::{ impls::actor_language::default_post_language, source::{ actor_language::CommunityLanguage, - comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, + comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, comment_reply::{CommentReply, CommentReplyUpdateForm}, local_site::LocalSite, person_mention::{PersonMention, PersonMentionUpdateForm}, @@ -56,7 +54,7 @@ pub async fn create_comment( let post_view = PostView::read( &mut context.pool(), post_id, - Some(local_user_view.person.id), + Some(&local_user_view.local_user), true, ) .await? @@ -126,25 +124,7 @@ pub async fn create_comment( .await .with_lemmy_type(LemmyErrorType::CouldntCreateComment)?; - // Necessary to update the ap_id let inserted_comment_id = inserted_comment.id; - let protocol_and_hostname = context.settings().get_protocol_and_hostname(); - - let apub_id = generate_local_apub_endpoint( - EndpointType::Comment, - &inserted_comment_id.to_string(), - &protocol_and_hostname, - )?; - let updated_comment = Comment::update( - &mut context.pool(), - inserted_comment_id, - &CommentUpdateForm { - ap_id: Some(apub_id), - ..Default::default() - }, - ) - .await - .with_lemmy_type(LemmyErrorType::CouldntCreateComment)?; // Scan the comment for user mentions, add those rows let mentions = scrape_text_for_mentions(&content); @@ -154,6 +134,7 @@ pub async fn create_comment( &local_user_view.person, true, &context, + Some(&local_user_view), ) .await?; @@ -170,7 +151,7 @@ pub async fn create_comment( .with_lemmy_type(LemmyErrorType::CouldntLikeComment)?; ActivityChannel::submit_activity( - SendActivityData::CreateComment(updated_comment.clone()), + SendActivityData::CreateComment(inserted_comment.clone()), &context, ) .await?; diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 2b1a20f89..8c81608c8 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -21,9 +21,13 @@ pub async fn delete_comment( local_user_view: LocalUserView, ) -> LemmyResult> { let comment_id = data.comment_id; - let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; // Dont delete it if its already been deleted. if orig_comment.comment.deleted == data.deleted { @@ -55,8 +59,15 @@ pub async fn delete_comment( .await .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; - let recipient_ids = - send_local_notifs(vec![], comment_id, &local_user_view.person, false, &context).await?; + let recipient_ids = send_local_notifs( + vec![], + comment_id, + &local_user_view.person, + false, + &context, + Some(&local_user_view), + ) + .await?; let updated_comment_id = updated_comment.id; ActivityChannel::submit_activity( diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 926472b94..ec4923e93 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -11,6 +11,7 @@ use lemmy_db_schema::{ source::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, + local_user::LocalUser, moderator::{ModRemoveComment, ModRemoveCommentForm}, }, traits::{Crud, Reportable}, @@ -25,9 +26,13 @@ pub async fn remove_comment( local_user_view: LocalUserView, ) -> LemmyResult> { let comment_id = data.comment_id; - let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_mod_action( &local_user_view.person, @@ -37,6 +42,14 @@ pub async fn remove_comment( ) .await?; + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + orig_comment.community.id, + local_user_view.person.id, + vec![orig_comment.creator.id], + ) + .await?; + // Don't allow removing or restoring comment which was deleted by user, as it would reveal // the comment text in mod log. if orig_comment.comment.deleted { @@ -71,9 +84,10 @@ pub async fn remove_comment( let recipient_ids = send_local_notifs( vec![], comment_id, - &local_user_view.person.clone(), + &local_user_view.person, false, &context, + Some(&local_user_view), ) .await?; let updated_comment_id = updated_comment.id; diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 4c8cf9436..ed9460825 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -36,9 +36,13 @@ pub async fn update_comment( let local_site = LocalSite::read(&mut context.pool()).await?; let comment_id = data.comment_id; - let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, @@ -87,6 +91,7 @@ pub async fn update_comment( &local_user_view.person, false, &context, + Some(&local_user_view), ) .await?; diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 0b0fad5dc..a0f0b7525 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -8,13 +8,11 @@ use lemmy_api_common::{ send_activity::SendActivityData, utils::{ check_community_user_action, - generate_local_apub_endpoint, get_url_blocklist, honeypot_check, local_site_to_slur_regex, mark_post_as_read, process_markdown_opt, - EndpointType, }, }; use lemmy_db_schema::{ @@ -23,7 +21,7 @@ use lemmy_db_schema::{ actor_language::CommunityLanguage, community::Community, local_site::LocalSite, - post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm}, + post::{Post, PostInsertForm, PostLike, PostLikeForm}, }, traits::{Crud, Likeable}, utils::diesel_url_create, @@ -37,11 +35,11 @@ use lemmy_utils::{ utils::{ slurs::check_slurs, validation::{ - check_url_scheme, is_url_blocked, is_valid_alt_text_field, is_valid_body_field, is_valid_post_title, + is_valid_url, }, }, }; @@ -71,11 +69,11 @@ pub async fn create_post( if let Some(url) = &url { is_url_blocked(url, &url_blocklist)?; - check_url_scheme(url)?; + is_valid_url(url)?; } if let Some(custom_thumbnail) = &custom_thumbnail { - check_url_scheme(custom_thumbnail)?; + is_valid_url(custom_thumbnail)?; } if let Some(alt_text) = &data.alt_text { @@ -147,26 +145,8 @@ pub async fn create_post( .await .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?; - let inserted_post_id = inserted_post.id; - let protocol_and_hostname = context.settings().get_protocol_and_hostname(); - let apub_id = generate_local_apub_endpoint( - EndpointType::Post, - &inserted_post_id.to_string(), - &protocol_and_hostname, - )?; - let updated_post = Post::update( - &mut context.pool(), - inserted_post_id, - &PostUpdateForm { - ap_id: Some(apub_id), - ..Default::default() - }, - ) - .await - .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?; - generate_post_link_metadata( - updated_post.clone(), + inserted_post.clone(), custom_thumbnail.map(Into::into), |post| Some(SendActivityData::CreatePost(post)), Some(local_site), @@ -189,11 +169,11 @@ pub async fn create_post( mark_post_as_read(person_id, post_id, &mut context.pool()).await?; - if let Some(url) = updated_post.url.clone() { + if let Some(url) = inserted_post.url.clone() { if community.visibility == CommunityVisibility::Public { spawn_try_task(async move { let mut webmention = - Webmention::new::(updated_post.ap_id.clone().into(), url.clone().into())?; + Webmention::new::(inserted_post.ap_id.clone().into(), url.clone().into())?; webmention.set_checked(true); match webmention .send() @@ -208,5 +188,5 @@ pub async fn create_post( } }; - build_post_response(&context, community_id, &local_user_view.person, post_id).await + 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 696566c8e..6834030ac 100644 --- a/crates/api_crud/src/post/delete.rs +++ b/crates/api_crud/src/post/delete.rs @@ -62,7 +62,7 @@ pub async fn delete_post( build_post_response( &context, orig_post.community_id, - &local_user_view.person, + local_user_view, data.post_id, ) .await diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 60b5609a9..ebf8940a2 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -55,9 +55,15 @@ pub async fn get_post( .await .is_ok(); - let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let local_user = local_user_view.map(|l| l.local_user); + let post_view = PostView::read( + &mut context.pool(), + post_id, + local_user.as_ref(), + is_mod_or_admin, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; let post_id = post_view.post.id; if let Some(person_id) = person_id { @@ -76,20 +82,19 @@ pub async fn get_post( let community_view = CommunityView::read( &mut context.pool(), community_id, - person_id, + local_user.as_ref(), is_mod_or_admin, ) .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; - let local_user = local_user_view.as_ref().map(|u| &u.local_user); // 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()), - local_user, + local_user: local_user.as_ref(), ..Default::default() } .list(&local_site.site, &mut context.pool()) diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index 682ed75d3..b4fdba6fb 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::{ + local_user::LocalUser, moderator::{ModRemovePost, ModRemovePostForm}, post::{Post, PostUpdateForm}, post_report::PostReport, @@ -37,6 +38,14 @@ pub async fn remove_post( ) .await?; + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + orig_post.community_id, + local_user_view.person.id, + vec![orig_post.creator_id], + ) + .await?; + // Update the post let post_id = data.post_id; let removed = data.removed; @@ -73,11 +82,5 @@ pub async fn remove_post( ) .await?; - build_post_response( - &context, - orig_post.community_id, - &local_user_view.person, - post_id, - ) - .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 9e665aed6..835398596 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -28,11 +28,11 @@ use lemmy_utils::{ utils::{ slurs::check_slurs, validation::{ - check_url_scheme, is_url_blocked, is_valid_alt_text_field, is_valid_body_field, is_valid_post_title, + is_valid_url, }, }, }; @@ -77,11 +77,11 @@ pub async fn update_post( if let Some(Some(url)) = &url { is_url_blocked(url, &url_blocklist)?; - check_url_scheme(url)?; + is_valid_url(url)?; } if let Some(Some(custom_thumbnail)) = &custom_thumbnail { - check_url_scheme(custom_thumbnail)?; + is_valid_url(custom_thumbnail)?; } let post_id = data.post_id; @@ -137,7 +137,7 @@ pub async fn update_post( build_post_response( context.deref(), orig_post.community_id, - &local_user_view.person, + local_user_view, post_id, ) .await diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 0381d196c..46908da6e 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -6,19 +6,17 @@ use lemmy_api_common::{ send_activity::{ActivityChannel, SendActivityData}, utils::{ check_person_block, - generate_local_apub_endpoint, get_interface_language, get_url_blocklist, local_site_to_slur_regex, process_markdown, send_email_to_user, - EndpointType, }, }; use lemmy_db_schema::{ source::{ local_site::LocalSite, - private_message::{PrivateMessage, PrivateMessageInsertForm, PrivateMessageUpdateForm}, + private_message::{PrivateMessage, PrivateMessageInsertForm}, }, traits::Crud, }; @@ -58,24 +56,6 @@ pub async fn create_private_message( .await .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?; - let inserted_private_message_id = inserted_private_message.id; - let protocol_and_hostname = context.settings().get_protocol_and_hostname(); - let apub_id = generate_local_apub_endpoint( - EndpointType::PrivateMessage, - &inserted_private_message_id.to_string(), - &protocol_and_hostname, - )?; - PrivateMessage::update( - &mut context.pool(), - inserted_private_message.id, - &PrivateMessageUpdateForm { - ap_id: Some(apub_id), - ..Default::default() - }, - ) - .await - .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?; - let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id) .await? .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 6b1909966..9dcd1595a 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -1,6 +1,6 @@ use crate::site::{application_question_check, site_default_post_listing_type_check}; -use activitypub_federation::http_signatures::generate_actor_keypair; -use actix_web::web::{Data, Json}; +use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair}; +use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, site::{CreateSite, SiteResponse}, diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 69e82007c..c633bebde 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -24,14 +24,14 @@ use lemmy_utils::{ VERSION, }; use moka::future::Cache; -use once_cell::sync::Lazy; +use std::sync::LazyLock; #[tracing::instrument(skip(context))] pub async fn get_site( local_user_view: Option, context: Data, ) -> LemmyResult> { - static CACHE: Lazy> = Lazy::new(|| { + static CACHE: LazyLock> = LazyLock::new(|| { Cache::builder() .max_capacity(1) .time_to_live(CACHE_DURATION_API) @@ -84,7 +84,7 @@ pub async fn get_site( |pool| CommunityBlockView::for_person(pool, person_id), |pool| InstanceBlockView::for_person(pool, person_id), |pool| PersonBlockView::for_person(pool, person_id), - |pool| CommunityModeratorView::for_person(pool, person_id, true), + |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)?; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index c84bd0a50..64bef8760 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -150,18 +150,18 @@ pub async fn register( .unwrap_or(site_view.site.content_warning.is_some()); // Create the local user - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .email(data.email.as_deref().map(str::to_lowercase)) - .password_encrypted(data.password.to_string()) - .show_nsfw(Some(show_nsfw)) - .accepted_application(accepted_application) - .default_listing_type(Some(local_site.default_post_listing_type)) - .post_listing_mode(Some(local_site.default_post_listing_mode)) - .interface_language(language_tags.first().cloned()) + 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), + 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)) - .build(); + admin: Some(!local_site.site_setup), + ..LocalUserInsertForm::new(inserted_person.id, data.password.to_string()) + }; let all_languages = Language::read_all(&mut context.pool()).await?; // use hashset to avoid duplicates diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index a9c6f657a..660489a68 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -31,7 +31,7 @@ serde = { workspace = true } actix-web = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -strum_macros = { workspace = true } +strum = { workspace = true } url = { workspace = true } http = { workspace = true } futures = { workspace = true } @@ -40,7 +40,6 @@ uuid = { workspace = true } async-trait = { workspace = true } anyhow = { workspace = true } reqwest = { workspace = true } -once_cell = { workspace = true } moka.workspace = true serde_with.workspace = true html2md = "0.2.14" diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs index ced50e2de..d42b62369 100644 --- a/crates/apub/src/activities/block/mod.rs +++ b/crates/apub/src/activities/block/mod.rs @@ -38,7 +38,6 @@ pub enum SiteOrCommunity { Site(ApubSite), Community(ApubCommunity), } - #[derive(Deserialize)] #[serde(untagged)] pub enum InstanceOrGroup { @@ -74,12 +73,18 @@ impl Object for SiteOrCommunity { }) } - async fn delete(self, _data: &Data) -> LemmyResult<()> { - unimplemented!() + async fn delete(self, data: &Data) -> LemmyResult<()> { + match self { + SiteOrCommunity::Site(i) => i.delete(data).await, + SiteOrCommunity::Community(c) => c.delete(data).await, + } } - async fn into_json(self, _data: &Data) -> LemmyResult { - unimplemented!() + async fn into_json(self, data: &Data) -> LemmyResult { + Ok(match self { + SiteOrCommunity::Site(i) => InstanceOrGroup::Instance(i.into_json(data).await?), + SiteOrCommunity::Community(c) => InstanceOrGroup::Group(c.into_json(data).await?), + }) } #[tracing::instrument(skip_all)] diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index 2406d2eb3..89be8d49e 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -179,7 +179,7 @@ impl ActivityHandler for CreateOrUpdateNote { // TODO: for compatibility with other projects, it would be much better to read this from cc or // tags let mentions = scrape_text_for_mentions(&comment.content); - send_local_notifs(mentions, comment.id, &actor, do_send_email, context).await?; + send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?; Ok(()) } } diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index cecc051b4..d203aacf2 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -175,8 +175,9 @@ pub(in crate::activities) async fn receive_remove_action( ) .await?; } - DeletableObjects::PrivateMessage(_) => unimplemented!(), - DeletableObjects::Person { .. } => unimplemented!(), + // TODO these need to be implemented yet, for now, return errors + DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::CouldntFindPrivateMessage)?, + DeletableObjects::Person(_) => Err(LemmyErrorType::CouldntFindPerson)?, } Ok(()) } diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index 4f0fad670..b50580852 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -155,8 +155,9 @@ impl UndoDelete { ) .await?; } - DeletableObjects::PrivateMessage(_) => unimplemented!(), - DeletableObjects::Person { .. } => unimplemented!(), + // TODO these need to be implemented yet, for now, return errors + DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::CouldntFindPrivateMessage)?, + DeletableObjects::Person(_) => Err(LemmyErrorType::CouldntFindPerson)?, } Ok(()) } diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index 3aeb7e45e..2d1fac449 100644 --- a/crates/apub/src/activity_lists.rs +++ b/crates/apub/src/activity_lists.rs @@ -26,7 +26,7 @@ use crate::{ }; use activitypub_federation::{config::Data, traits::ActivityHandler}; use lemmy_api_common::context::LemmyContext; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; use url::Url; @@ -117,7 +117,7 @@ impl InCommunity for AnnouncableActivities { CollectionRemove(a) => a.community(context).await, LockPost(a) => a.community(context).await, UndoLockPost(a) => a.community(context).await, - Page(_) => unimplemented!(), + Page(_) => Err(LemmyErrorType::CouldntFindPost.into()), } } } diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index d83b9a135..12d18110e 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -37,11 +37,11 @@ pub async fn list_comments( }; let sort = data.sort; let max_depth = data.max_depth; - let saved_only = data.saved_only.unwrap_or_default(); + let saved_only = data.saved_only; - let liked_only = data.liked_only.unwrap_or_default(); - let disliked_only = data.disliked_only.unwrap_or_default(); - if liked_only && disliked_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)); } diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 31bdd0c8e..7ceafed8d 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -40,12 +40,14 @@ pub async fn list_posts( } else { data.community_id }; - let saved_only = data.saved_only.unwrap_or_default(); - let show_hidden = data.show_hidden.unwrap_or_default(); + let saved_only = data.saved_only; + let show_hidden = data.show_hidden; + let show_read = data.show_read; + let show_nsfw = data.show_nsfw; - let liked_only = data.liked_only.unwrap_or_default(); - let disliked_only = data.disliked_only.unwrap_or_default(); - if liked_only && disliked_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)); } @@ -82,6 +84,8 @@ pub async fn list_posts( page_after, limit, show_hidden, + show_read, + show_nsfw, ..Default::default() } .list(&local_site.site, &mut context.pool()) diff --git a/crates/apub/src/api/read_community.rs b/crates/apub/src/api/read_community.rs index dae7719ae..62fd6ec0b 100644 --- a/crates/apub/src/api/read_community.rs +++ b/crates/apub/src/api/read_community.rs @@ -29,7 +29,7 @@ pub async fn get_community( check_private_instance(&local_user_view, &local_site)?; - let person_id = local_user_view.as_ref().map(|u| u.person.id); + let local_user = local_user_view.as_ref().map(|u| &u.local_user); let community_id = match data.id { Some(id) => id, @@ -53,7 +53,7 @@ pub async fn get_community( let community_view = CommunityView::read( &mut context.pool(), community_id, - person_id, + local_user, is_mod_or_admin, ) .await? diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index a6b3560aa..d61f6d9c5 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -55,11 +55,11 @@ pub async fn read_person( let sort = data.sort; let page = data.page; let limit = data.limit; - let saved_only = data.saved_only.unwrap_or_default(); + let saved_only = data.saved_only; let community_id = data.community_id; // If its saved only, you don't care what creator it was // Or, if its not saved, then you only want it for that specific creator - let creator_id = if !saved_only { + let creator_id = if !saved_only.unwrap_or_default() { Some(person_details_id) } else { None @@ -96,7 +96,7 @@ pub async fn read_person( let moderates = CommunityModeratorView::for_person( &mut context.pool(), person_details_id, - local_user_view.is_some(), + local_user_view.map(|l| l.local_user).as_ref(), ) .await?; diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index 47f6c5d06..3f2591241 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ site::{ResolveObject, ResolveObjectResponse}, utils::check_private_instance, }; -use lemmy_db_schema::{newtypes::PersonId, source::local_site::LocalSite, utils::DbPool}; +use lemmy_db_schema::{source::local_site::LocalSite, utils::DbPool}; use lemmy_db_views::structs::{CommentView, LocalUserView, PostView}; use lemmy_db_views_actor::structs::{CommunityView, PersonView}; use lemmy_utils::error::{LemmyErrorExt2, LemmyErrorType, LemmyResult}; @@ -23,10 +23,9 @@ pub async fn resolve_object( ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site)?; - let person_id = local_user_view.map(|v| v.person.id); // If we get a valid personId back we can safely assume that the user is authenticated, // if there's no personId then the JWT was missing or invalid. - let is_authenticated = person_id.is_some(); + let is_authenticated = local_user_view.is_some(); let res = if is_authenticated { // user is fully authenticated; allow remote lookups as well. @@ -37,24 +36,26 @@ pub async fn resolve_object( } .with_lemmy_type(LemmyErrorType::CouldntFindObject)?; - convert_response(res, person_id, &mut context.pool()) + convert_response(res, local_user_view, &mut context.pool()) .await .with_lemmy_type(LemmyErrorType::CouldntFindObject) } async fn convert_response( object: SearchableObjects, - user_id: Option, + 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); + match object { Post(p) => { removed_or_deleted = p.deleted || p.removed; res.post = Some( - PostView::read(pool, p.id, user_id, false) + PostView::read(pool, p.id, local_user.as_ref(), false) .await? .ok_or(LemmyErrorType::CouldntFindPost)?, ) @@ -62,7 +63,7 @@ async fn convert_response( Comment(c) => { removed_or_deleted = c.deleted || c.removed; res.comment = Some( - CommentView::read(pool, c.id, user_id) + CommentView::read(pool, c.id, local_user.as_ref()) .await? .ok_or(LemmyErrorType::CouldntFindComment)?, ) @@ -79,7 +80,7 @@ async fn convert_response( UserOrCommunity::Community(c) => { removed_or_deleted = c.deleted || c.removed; res.community = Some( - CommunityView::read(pool, c.id, user_id, false) + CommunityView::read(pool, c.id, local_user.as_ref(), false) .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?, ) diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 9f2cb58c5..a0879b3c9 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -345,10 +345,7 @@ mod tests { }; let person = Person::create(&mut context.pool(), &person_form).await?; - let user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("pass".to_string()) - .build(); + let user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?; Ok( diff --git a/crates/apub/src/fetcher/post_or_comment.rs b/crates/apub/src/fetcher/post_or_comment.rs index 083369b9d..e352e1257 100644 --- a/crates/apub/src/fetcher/post_or_comment.rs +++ b/crates/apub/src/fetcher/post_or_comment.rs @@ -61,8 +61,11 @@ impl Object for PostOrComment { } } - async fn into_json(self, _data: &Data) -> LemmyResult { - unimplemented!() + 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?), + }) } #[tracing::instrument(skip_all)] diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 8c533ba88..76c284820 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -118,8 +118,17 @@ impl Object for SearchableObjects { } } - async fn into_json(self, _data: &Data) -> LemmyResult { - unimplemented!() + async fn into_json(self, data: &Data) -> LemmyResult { + 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?), + })) + } + }) } #[tracing::instrument(skip_all)] 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 30b5fd568..c6a1bb17e 100644 --- a/crates/apub/src/fetcher/site_or_community_or_user.rs +++ b/crates/apub/src/fetcher/site_or_community_or_user.rs @@ -1,6 +1,6 @@ use crate::{ fetcher::user_or_community::{PersonOrGroup, UserOrCommunity}, - objects::instance::ApubSite, + objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson}, protocol::objects::instance::Instance, }; use activitypub_federation::{ @@ -41,11 +41,14 @@ impl Object for SiteOrCommunityOrUser { } #[tracing::instrument(skip_all)] - async fn read_from_id( - _object_id: Url, - _data: &Data, - ) -> LemmyResult> { - unimplemented!(); + async fn read_from_id(object_id: Url, data: &Data) -> LemmyResult> { + let site = ApubSite::read_from_id(object_id.clone(), data).await?; + Ok(match site { + Some(o) => Some(SiteOrCommunityOrUser::Site(o)), + None => UserOrCommunity::read_from_id(object_id, data) + .await? + .map(SiteOrCommunityOrUser::UserOrCommunity), + }) } #[tracing::instrument(skip_all)] @@ -56,8 +59,13 @@ impl Object for SiteOrCommunityOrUser { } } - async fn into_json(self, _data: &Data) -> LemmyResult { - unimplemented!() + async fn into_json(self, data: &Data) -> LemmyResult { + Ok(match self { + SiteOrCommunityOrUser::Site(p) => SiteOrPersonOrGroup::Instance(p.into_json(data).await?), + SiteOrCommunityOrUser::UserOrCommunity(p) => { + SiteOrPersonOrGroup::PersonOrGroup(p.into_json(data).await?) + } + }) } #[tracing::instrument(skip_all)] @@ -75,8 +83,18 @@ impl Object for SiteOrCommunityOrUser { } #[tracing::instrument(skip_all)] - async fn from_json(_apub: Self::Kind, _data: &Data) -> LemmyResult { - unimplemented!(); + async fn from_json(apub: Self::Kind, data: &Data) -> LemmyResult { + Ok(match apub { + SiteOrPersonOrGroup::Instance(a) => { + SiteOrCommunityOrUser::Site(ApubSite::from_json(a, data).await?) + } + SiteOrPersonOrGroup::PersonOrGroup(a) => SiteOrCommunityOrUser::UserOrCommunity(match a { + PersonOrGroup::Person(p) => UserOrCommunity::User(ApubPerson::from_json(p, data).await?), + PersonOrGroup::Group(g) => { + UserOrCommunity::Community(ApubCommunity::from_json(g, data).await?) + } + }), + }) } } @@ -103,6 +121,9 @@ impl Actor for SiteOrCommunityOrUser { } fn inbox(&self) -> Url { - unimplemented!() + match self { + SiteOrCommunityOrUser::Site(u) => u.inbox(), + SiteOrCommunityOrUser::UserOrCommunity(c) => c.inbox(), + } } } diff --git a/crates/apub/src/fetcher/user_or_community.rs b/crates/apub/src/fetcher/user_or_community.rs index d29cbb6b0..129af8803 100644 --- a/crates/apub/src/fetcher/user_or_community.rs +++ b/crates/apub/src/fetcher/user_or_community.rs @@ -65,8 +65,11 @@ impl Object for UserOrCommunity { } } - async fn into_json(self, _data: &Data) -> LemmyResult { - unimplemented!() + async fn into_json(self, data: &Data) -> LemmyResult { + Ok(match self { + UserOrCommunity::User(p) => PersonOrGroup::Person(p.into_json(data).await?), + UserOrCommunity::Community(p) => PersonOrGroup::Group(p.into_json(data).await?), + }) } #[tracing::instrument(skip_all)] @@ -115,7 +118,10 @@ impl Actor for UserOrCommunity { } fn inbox(&self) -> Url { - unimplemented!() + match self { + UserOrCommunity::User(p) => p.inbox(), + UserOrCommunity::Community(p) => p.inbox(), + } } } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index c8960b008..c8506da52 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -14,9 +14,8 @@ use lemmy_utils::{ CACHE_DURATION_FEDERATION, }; use moka::future::Cache; -use once_cell::sync::Lazy; use serde_json::Value; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use url::Url; pub mod activities; @@ -36,7 +35,7 @@ pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 100; /// Only include a basic context to save space and bandwidth. The main context is hosted statically /// on join-lemmy.org. Include activitystreams explicitly for better compat, but this could /// theoretically also be moved. -pub static FEDERATION_CONTEXT: Lazy = Lazy::new(|| { +pub static FEDERATION_CONTEXT: LazyLock = LazyLock::new(|| { Value::Array(vec![ Value::String("https://join-lemmy.org/context.json".to_string()), Value::String("https://www.w3.org/ns/activitystreams".to_string()), @@ -129,7 +128,7 @@ pub(crate) async fn local_site_data_cached( // multiple times. This causes a huge number of database reads if we hit the db directly. So we // cache these values for a short time, which will already make a huge difference and ensures that // changes take effect quickly. - static CACHE: Lazy>> = Lazy::new(|| { + static CACHE: LazyLock>> = LazyLock::new(|| { Cache::builder() .max_capacity(1) .time_to_live(CACHE_DURATION_FEDERATION) diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index 24bb63b4c..c67a223e0 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -88,7 +88,7 @@ impl Object for ApubSite { } async fn delete(self, _data: &Data) -> LemmyResult<()> { - unimplemented!() + Err(LemmyErrorType::CantDeleteSite.into()) } #[tracing::instrument(skip_all)] @@ -109,7 +109,7 @@ impl Object for ApubSite { icon: self.icon.clone().map(ImageObject::new), image: self.banner.clone().map(ImageObject::new), inbox: self.inbox_url.clone().into(), - outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?, + outbox: Url::parse(&format!("{}site_outbox", self.actor_id))?, public_key: self.public_key(), language, content_warning: self.content_warning.clone(), diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 7e4254840..44e842413 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -41,7 +41,11 @@ use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_utils::{ error::{LemmyError, LemmyErrorType, LemmyResult}, spawn_try_task, - utils::{markdown::markdown_to_html, slurs::check_slurs_opt, validation::check_url_scheme}, + utils::{ + markdown::markdown_to_html, + slurs::check_slurs_opt, + validation::{is_url_blocked, is_valid_url}, + }, }; use std::ops::Deref; use stringreader::StringReader; @@ -180,8 +184,15 @@ impl Object for ApubPost { let creator = page.creator()?.dereference(context).await?; let community = page.community(context).await?; if community.posting_restricted_to_mods { - CommunityModeratorView::is_community_moderator(&mut context.pool(), community.id, creator.id) - .await?; + let is_mod = CommunityModeratorView::is_community_moderator( + &mut context.pool(), + community.id, + creator.id, + ) + .await?; + if !is_mod { + Err(LemmyErrorType::OnlyModsCanPostInCommunity)? + } } let mut name = page .name @@ -220,14 +231,16 @@ impl Object for ApubPost { None }; + let url_blocklist = get_url_blocklist(context).await?; + if let Some(url) = &url { - check_url_scheme(url)?; + is_url_blocked(url, &url_blocklist)?; + is_valid_url(url)?; } let alt_text = first_attachment.cloned().and_then(Attachment::alt_text); let slur_regex = &local_site_opt_to_slur_regex(&local_site); - let url_blocklist = get_url_blocklist(context).await?; 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?; diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 35f2fe418..fc9697391 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -73,7 +73,7 @@ impl Object for ApubPrivateMessage { async fn delete(self, _context: &Data) -> LemmyResult<()> { // do nothing, because pm can't be fetched over http - unimplemented!() + Err(LemmyErrorType::CouldntFindPrivateMessage.into()) } #[tracing::instrument(skip_all)] diff --git a/crates/apub/src/protocol/activities/community/lock_page.rs b/crates/apub/src/protocol/activities/community/lock_page.rs index c60b86cf8..a08b3c5a2 100644 --- a/crates/apub/src/protocol/activities/community/lock_page.rs +++ b/crates/apub/src/protocol/activities/community/lock_page.rs @@ -13,10 +13,10 @@ use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::Crud}; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; -use strum_macros::Display; +use strum::Display; use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, Display)] +#[derive(Clone, Debug, Display, Deserialize, Serialize)] pub enum LockType { Lock, } diff --git a/crates/apub/src/protocol/activities/mod.rs b/crates/apub/src/protocol/activities/mod.rs index ce0808ee9..d1da73a16 100644 --- a/crates/apub/src/protocol/activities/mod.rs +++ b/crates/apub/src/protocol/activities/mod.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use strum_macros::Display; +use strum::Display; pub mod block; pub mod community; diff --git a/crates/apub/src/protocol/activities/voting/vote.rs b/crates/apub/src/protocol/activities/voting/vote.rs index b632333c7..9fae264a5 100644 --- a/crates/apub/src/protocol/activities/voting/vote.rs +++ b/crates/apub/src/protocol/activities/voting/vote.rs @@ -8,7 +8,7 @@ use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; use lemmy_api_common::context::LemmyContext; use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; -use strum_macros::Display; +use strum::Display; use url::Url; #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/crates/apub/src/protocol/objects/group.rs b/crates/apub/src/protocol/objects/group.rs index a95fff262..8f138e001 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, verify_is_remote_object}, + objects::{community::ApubCommunity, read_from_string_or_source_opt}, protocol::{ objects::{Endpoints, LanguageTag}, ImageObject, @@ -80,7 +80,6 @@ impl Group { ) -> LemmyResult<()> { check_apub_id_valid_with_strictness(self.id.inner(), true, context).await?; verify_domains_match(expected_domain, self.id.inner())?; - verify_is_remote_object(&self.id, context)?; let local_site_data = local_site_data_cached(&mut context.pool()).await?; let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site); diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs index 6075b14a1..9c37c88c3 100644 --- a/crates/apub/src/protocol/objects/page.rs +++ b/crates/apub/src/protocol/objects/page.rs @@ -193,10 +193,12 @@ impl ActivityHandler for Page { type DataType = LemmyContext; type Error = LemmyError; fn id(&self) -> &Url { - unimplemented!() + self.id.inner() } + fn actor(&self) -> &Url { - unimplemented!() + debug_assert!(false); + self.id.inner() } async fn verify(&self, data: &Data) -> LemmyResult<()> { ApubPost::verify(self, self.id.inner(), data).await diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index dc924fa91..57af68f46 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -27,7 +27,6 @@ full = [ "lemmy_utils", "activitypub_federation", "regex", - "once_cell", "serde_json", "diesel_ltree", "diesel-async", @@ -46,7 +45,6 @@ serde = { workspace = true } serde_with = { workspace = true } url = { workspace = true } strum = { workspace = true } -strum_macros = { workspace = true } serde_json = { workspace = true, optional = true } activitypub_federation = { workspace = true, optional = true } lemmy_utils = { workspace = true, optional = true } @@ -65,7 +63,6 @@ diesel-async = { workspace = true, features = [ "deadpool", ], optional = true } regex = { workspace = true, optional = true } -once_cell = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true } typed-builder = { workspace = true } async-trait = { workspace = true } diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 87866e89c..973d3325f 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -564,6 +564,10 @@ BEGIN IF NOT (NEW.path ~ ('*.' || id)::lquery) THEN NEW.path = NEW.path || id; END IF; + -- Set local ap_id + IF NEW.local THEN + NEW.ap_id = coalesce(NEW.ap_id, r.local_url ('/comment/' || id)); + END IF; RETURN NEW; END $$; @@ -573,3 +577,39 @@ CREATE TRIGGER change_values FOR EACH ROW EXECUTE FUNCTION r.comment_change_values (); +CREATE FUNCTION r.post_change_values () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + -- Set local ap_id + IF NEW.local THEN + NEW.ap_id = coalesce(NEW.ap_id, r.local_url ('/post/' || NEW.id::text)); + END IF; + RETURN NEW; +END +$$; + +CREATE TRIGGER change_values + BEFORE INSERT ON post + FOR EACH ROW + EXECUTE FUNCTION r.post_change_values (); + +CREATE FUNCTION r.private_message_change_values () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + -- Set local ap_id + IF NEW.local THEN + NEW.ap_id = coalesce(NEW.ap_id, r.local_url ('/private_message/' || NEW.id::text)); + END IF; + RETURN NEW; +END +$$; + +CREATE TRIGGER change_values + BEFORE INSERT ON private_message + FOR EACH ROW + EXECUTE FUNCTION r.private_message_change_values (); + diff --git a/crates/db_schema/replaceable_schema/utils.sql b/crates/db_schema/replaceable_schema/utils.sql index f236c5387..c766d25f2 100644 --- a/crates/db_schema/replaceable_schema/utils.sql +++ b/crates/db_schema/replaceable_schema/utils.sql @@ -8,7 +8,7 @@ CREATE FUNCTION r.controversy_rank (upvotes numeric, downvotes numeric) 0 ELSE ( - upvotes + downvotes) * CASE WHEN upvotes > downvotes THEN + upvotes + downvotes) ^ CASE WHEN upvotes > downvotes THEN downvotes::float / upvotes::float ELSE upvotes::float / downvotes::float @@ -57,6 +57,13 @@ BEGIN END; $$; +CREATE FUNCTION r.local_url (url_path text) + RETURNS text + LANGUAGE sql + STABLE PARALLEL SAFE RETURN ( +current_setting('lemmy.protocol_and_hostname') || url_path +); + -- This function creates statement-level triggers for all operation types. It's designed this way -- because of these limitations: -- * A trigger that uses transition tables can only handle 1 operation type. diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index 8483d6c20..5a8658baf 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -533,10 +533,7 @@ mod tests { let person_form = PersonInsertForm::test_form(instance.id, "my test person"); let person = Person::create(pool, &person_form).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("my_pw".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await @@ -645,10 +642,7 @@ mod tests { let person_form = PersonInsertForm::test_form(instance.id, "my test person"); let person = Person::create(pool, &person_form).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("my_pw".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await .unwrap(); diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 0ffd53f86..977bc9083 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -117,7 +117,7 @@ impl Crud for Comment { type UpdateForm = CommentUpdateForm; type IdType = CommentId; - /// This is unimplemented, use [[Comment::create]] + /// Use [[Comment::create]] async fn create(pool: &mut DbPool<'_>, comment_form: &Self::InsertForm) -> Result { debug_assert!(false); Comment::create(pool, comment_form, None).await @@ -223,6 +223,7 @@ mod tests { use diesel_ltree::Ltree; use pretty_assertions::assert_eq; use serial_test::serial; + use url::Url; #[tokio::test] #[serial] @@ -273,7 +274,12 @@ mod tests { path: Ltree(format!("0.{}", inserted_comment.id)), published: inserted_comment.published, updated: None, - ap_id: inserted_comment.ap_id.clone(), + ap_id: Url::parse(&format!( + "https://lemmy-alpha/comment/{}", + inserted_comment.id + )) + .unwrap() + .into(), distinguished: false, local: true, language_id: LanguageId::default(), diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 6cd90cc66..eaf35a90d 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -1,7 +1,14 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId}, - schema::{community, community_follower, instance}, + schema::{ + community, + community_follower, + community_moderator, + community_person_ban, + instance, + post, + }, source::{ actor_language::CommunityLanguage, community::{ @@ -42,6 +49,7 @@ use diesel::{ Queryable, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[async_trait] impl Crud for Community { @@ -83,9 +91,8 @@ impl Joinable for CommunityModerator { pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, ) -> Result { - use crate::schema::community_moderator::dsl::community_moderator; let conn = &mut get_conn(pool).await?; - insert_into(community_moderator) + insert_into(community_moderator::table) .values(community_moderator_form) .get_result::(conn) .await @@ -95,9 +102,8 @@ impl Joinable for CommunityModerator { pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, ) -> Result { - use crate::schema::community_moderator::dsl::community_moderator; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.find(( + diesel::delete(community_moderator::table.find(( community_moderator_form.person_id, community_moderator_form.community_id, ))) @@ -147,25 +153,23 @@ impl Community { pool: &mut DbPool<'_>, url: &DbUrl, ) -> Result, Error> { - use crate::schema::community::dsl::{featured_url, moderators_url}; - use CollectionType::*; let conn = &mut get_conn(pool).await?; let res = community::table - .filter(moderators_url.eq(url)) + .filter(community::moderators_url.eq(url)) .first(conn) .await .optional()?; if let Some(c) = res { - Ok(Some((c, Moderators))) + Ok(Some((c, CollectionType::Moderators))) } else { let res = community::table - .filter(featured_url.eq(url)) + .filter(community::featured_url.eq(url)) .first(conn) .await .optional()?; if let Some(c) = res { - Ok(Some((c, Featured))) + Ok(Some((c, CollectionType::Featured))) } else { Ok(None) } @@ -177,7 +181,6 @@ impl Community { posts: Vec, pool: &mut DbPool<'_>, ) -> Result<(), Error> { - use crate::schema::post; let conn = &mut get_conn(pool).await?; for p in &posts { debug_assert!(p.community_id == community_id); @@ -185,10 +188,10 @@ impl Community { // Mark the given posts as featured and all other posts as not featured. let post_ids = posts.iter().map(|p| p.id); update(post::table) - .filter(post::dsl::community_id.eq(community_id)) + .filter(post::community_id.eq(community_id)) // This filter is just for performance - .filter(post::dsl::featured_community.or(post::dsl::id.eq_any(post_ids.clone()))) - .set(post::dsl::featured_community.eq(post::dsl::id.eq_any(post_ids))) + .filter(post::featured_community.or(post::id.eq_any(post_ids.clone()))) + .set(post::featured_community.eq(post::id.eq_any(post_ids))) .execute(conn) .await?; Ok(()) @@ -200,37 +203,68 @@ impl CommunityModerator { pool: &mut DbPool<'_>, for_community_id: CommunityId, ) -> Result { - use crate::schema::community_moderator::dsl::{community_id, community_moderator}; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.filter(community_id.eq(for_community_id))) - .execute(conn) - .await + diesel::delete( + community_moderator::table.filter(community_moderator::community_id.eq(for_community_id)), + ) + .execute(conn) + .await } pub async fn leave_all_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, ) -> Result { - use crate::schema::community_moderator::dsl::{community_moderator, person_id}; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.filter(person_id.eq(for_person_id))) - .execute(conn) - .await + diesel::delete( + community_moderator::table.filter(community_moderator::person_id.eq(for_person_id)), + ) + .execute(conn) + .await } pub async fn get_person_moderated_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, ) -> Result, Error> { - use crate::schema::community_moderator::dsl::{community_id, community_moderator, person_id}; let conn = &mut get_conn(pool).await?; - community_moderator - .filter(person_id.eq(for_person_id)) - .select(community_id) + community_moderator::table + .filter(community_moderator::person_id.eq(for_person_id)) + .select(community_moderator::community_id) .load::(conn) .await } + + /// Checks to make sure the acting moderator was added earlier than the target moderator + pub async fn is_higher_mod_check( + pool: &mut DbPool<'_>, + for_community_id: CommunityId, + mod_person_id: PersonId, + target_person_ids: Vec, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + + // Build the list of persons + let mut persons = target_person_ids; + 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) + // This does a limit 1 select first + .first::(conn) + .await?; + + // If the first result sorted by published is the acting mod + if res.person_id == mod_person_id { + Ok(()) + } else { + Err(LemmyErrorType::NotHigherMod)? + } + } } #[async_trait] @@ -240,11 +274,13 @@ impl Bannable for CommunityPersonBan { pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, ) -> Result { - use crate::schema::community_person_ban::dsl::{community_id, community_person_ban, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(community_person_ban) + insert_into(community_person_ban::table) .values(community_person_ban_form) - .on_conflict((community_id, person_id)) + .on_conflict(( + community_person_ban::community_id, + community_person_ban::person_id, + )) .do_update() .set(community_person_ban_form) .get_result::(conn) @@ -255,9 +291,8 @@ impl Bannable for CommunityPersonBan { pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, ) -> Result { - use crate::schema::community_person_ban::dsl::community_person_ban; let conn = &mut get_conn(pool).await?; - diesel::delete(community_person_ban.find(( + diesel::delete(community_person_ban::table.find(( community_person_ban_form.person_id, community_person_ban_form.community_id, ))) @@ -291,11 +326,10 @@ impl CommunityFollower { pool: &mut DbPool<'_>, remote_community_id: CommunityId, ) -> Result { - use crate::schema::community_follower::dsl::{community_follower, community_id}; let conn = &mut get_conn(pool).await?; - select(exists( - community_follower.filter(community_id.eq(remote_community_id)), - )) + select(exists(community_follower::table.filter( + community_follower::community_id.eq(remote_community_id), + ))) .get_result(conn) .await } @@ -316,11 +350,13 @@ impl Queryable, Pg> for SubscribedType { impl Followable for CommunityFollower { type Form = CommunityFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { - use crate::schema::community_follower::dsl::{community_follower, community_id, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(community_follower) + insert_into(community_follower::table) .values(form) - .on_conflict((community_id, person_id)) + .on_conflict(( + community_follower::community_id, + community_follower::person_id, + )) .do_update() .set(form) .get_result::(conn) @@ -331,17 +367,16 @@ impl Followable for CommunityFollower { community_id: CommunityId, person_id: PersonId, ) -> Result { - use crate::schema::community_follower::dsl::{community_follower, pending}; let conn = &mut get_conn(pool).await?; - diesel::update(community_follower.find((person_id, community_id))) - .set(pending.eq(false)) + diesel::update(community_follower::table.find((person_id, community_id))) + .set(community_follower::pending.eq(false)) .get_result::(conn) .await } + async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { - use crate::schema::community_follower::dsl::community_follower; let conn = &mut get_conn(pool).await?; - diesel::delete(community_follower.find((form.person_id, form.community_id))) + diesel::delete(community_follower::table.find((form.person_id, form.community_id))) .execute(conn) .await } @@ -397,10 +432,8 @@ impl ApubActor for Community { } #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { - use crate::{ source::{ community::{ @@ -415,28 +448,30 @@ mod tests { CommunityUpdateForm, }, instance::Instance, + local_user::LocalUser, person::{Person, PersonInsertForm}, }, traits::{Bannable, Crud, Followable, Joinable}, utils::build_db_pool_for_tests, CommunityVisibility, }; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; 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, "bobbee"); + let bobby_person = PersonInsertForm::test_form(inserted_instance.id, "bobby"); + let inserted_bobby = Person::create(pool, &bobby_person).await?; - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + 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()) @@ -445,7 +480,7 @@ mod tests { .instance_id(inserted_instance.id) .build(); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let expected_community = Community { id: inserted_community.id, @@ -477,91 +512,120 @@ mod tests { let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, pending: false, }; - let inserted_community_follower = CommunityFollower::follow(pool, &community_follower_form) - .await - .unwrap(); + let inserted_community_follower = + CommunityFollower::follow(pool, &community_follower_form).await?; let expected_community_follower = CommunityFollower { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, pending: false, published: inserted_community_follower.published, }; - let community_moderator_form = CommunityModeratorForm { + let bobby_moderator_form = CommunityModeratorForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, }; - let inserted_community_moderator = CommunityModerator::join(pool, &community_moderator_form) - .await - .unwrap(); + let inserted_bobby_moderator = CommunityModerator::join(pool, &bobby_moderator_form).await?; + + let artemis_moderator_form = CommunityModeratorForm { + community_id: inserted_community.id, + person_id: inserted_artemis.id, + }; + + let _inserted_artemis_moderator = + CommunityModerator::join(pool, &artemis_moderator_form).await?; let expected_community_moderator = CommunityModerator { community_id: inserted_community.id, - person_id: inserted_person.id, - published: inserted_community_moderator.published, + person_id: inserted_bobby.id, + published: inserted_bobby_moderator.published, }; + let moderator_person_ids = vec![inserted_bobby.id, inserted_artemis.id]; + + // Make sure bobby is marked as a higher mod than artemis, and vice versa + let bobby_higher_check = CommunityModerator::is_higher_mod_check( + pool, + inserted_community.id, + inserted_bobby.id, + moderator_person_ids.clone(), + ) + .await; + assert!(bobby_higher_check.is_ok()); + + // Also check the other is_higher_mod_or_admin function just in case + let bobby_higher_check_2 = LocalUser::is_higher_mod_or_admin_check( + pool, + inserted_community.id, + inserted_bobby.id, + moderator_person_ids.clone(), + ) + .await; + assert!(bobby_higher_check_2.is_ok()); + + // This should throw an error, since artemis was added later + let artemis_higher_check = CommunityModerator::is_higher_mod_check( + pool, + inserted_community.id, + inserted_artemis.id, + moderator_person_ids, + ) + .await; + assert!(artemis_higher_check.is_err()); + let community_person_ban_form = CommunityPersonBanForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, expires: None, }; - let inserted_community_person_ban = CommunityPersonBan::ban(pool, &community_person_ban_form) - .await - .unwrap(); + let inserted_community_person_ban = + CommunityPersonBan::ban(pool, &community_person_ban_form).await?; let expected_community_person_ban = CommunityPersonBan { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, published: inserted_community_person_ban.published, expires: None, }; let read_community = Community::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let update_community_form = CommunityUpdateForm { title: Some("nada".to_owned()), ..Default::default() }; - let updated_community = Community::update(pool, inserted_community.id, &update_community_form) - .await - .unwrap(); + let updated_community = + Community::update(pool, inserted_community.id, &update_community_form).await?; - let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form) - .await - .unwrap(); - let left_community = CommunityModerator::leave(pool, &community_moderator_form) - .await - .unwrap(); - let unban = CommunityPersonBan::unban(pool, &community_person_ban_form) - .await - .unwrap(); - let 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 ignored_community = CommunityFollower::unfollow(pool, &community_follower_form).await?; + let left_community = CommunityModerator::leave(pool, &bobby_moderator_form).await?; + let unban = CommunityPersonBan::unban(pool, &community_person_ban_form).await?; + let num_deleted = Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_bobby.id).await?; + Person::delete(pool, inserted_artemis.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_community, read_community); assert_eq!(expected_community, inserted_community); assert_eq!(expected_community, updated_community); assert_eq!(expected_community_follower, inserted_community_follower); - assert_eq!(expected_community_moderator, inserted_community_moderator); + 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!(2, loaded_count); assert_eq!(1, num_deleted); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/images.rs b/crates/db_schema/src/impls/images.rs index 9589aeee3..547bfc4e2 100644 --- a/crates/db_schema/src/impls/images.rs +++ b/crates/db_schema/src/impls/images.rs @@ -1,7 +1,14 @@ use crate::{ newtypes::DbUrl, - schema::{local_image, remote_image}, - source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm}, + schema::{image_details, local_image, remote_image}, + source::images::{ + ImageDetails, + ImageDetailsForm, + LocalImage, + LocalImageForm, + RemoteImage, + RemoteImageForm, + }, utils::{get_conn, DbPool}, }; use diesel::{ @@ -13,15 +20,29 @@ use diesel::{ NotFound, QueryDsl, }; -use diesel_async::RunQueryDsl; -use url::Url; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; impl LocalImage { - pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result { + pub async fn create( + pool: &mut DbPool<'_>, + form: &LocalImageForm, + image_details_form: &ImageDetailsForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(local_image::table) - .values(form) - .get_result::(conn) + conn + .build_transaction() + .run(|conn| { + Box::pin(async move { + let local_insert = insert_into(local_image::table) + .values(form) + .get_result::(conn) + .await; + + ImageDetails::create(conn, image_details_form).await?; + + local_insert + }) as _ + }) .await } @@ -39,16 +60,26 @@ impl LocalImage { } impl RemoteImage { - pub async fn create(pool: &mut DbPool<'_>, links: Vec) -> Result { + pub async fn create(pool: &mut DbPool<'_>, form: &ImageDetailsForm) -> Result { let conn = &mut get_conn(pool).await?; - let forms = links - .into_iter() - .map(|url| RemoteImageForm { link: url.into() }) - .collect::>(); - insert_into(remote_image::table) - .values(forms) - .on_conflict_do_nothing() - .execute(conn) + 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 _ + }) .await } @@ -67,3 +98,16 @@ impl RemoteImage { } } } + +impl ImageDetails { + pub(crate) async fn create( + conn: &mut AsyncPgConnection, + form: &ImageDetailsForm, + ) -> Result { + insert_into(image_details::table) + .values(form) + .on_conflict_do_nothing() + .execute(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/local_site.rs b/crates/db_schema/src/impls/local_site.rs index 602dfe1f4..926814c48 100644 --- a/crates/db_schema/src/impls/local_site.rs +++ b/crates/db_schema/src/impls/local_site.rs @@ -7,7 +7,7 @@ use diesel::{dsl::insert_into, result::Error}; use diesel_async::RunQueryDsl; use lemmy_utils::{error::LemmyResult, CACHE_DURATION_API}; use moka::future::Cache; -use once_cell::sync::Lazy; +use std::sync::LazyLock; impl LocalSite { pub async fn create(pool: &mut DbPool<'_>, form: &LocalSiteInsertForm) -> Result { @@ -18,7 +18,7 @@ impl LocalSite { .await } pub async fn read(pool: &mut DbPool<'_>) -> LemmyResult { - static CACHE: Lazy> = Lazy::new(|| { + static CACHE: LazyLock> = LazyLock::new(|| { Cache::builder() .max_capacity(1) .time_to_live(CACHE_DURATION_API) diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 9b59e07ba..acff6af2a 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::{DbUrl, LanguageId, LocalUserId, PersonId}, - schema::{local_user, person, registration_application}, + newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId}, + schema::{community, community_moderator, local_user, person, registration_application}, source::{ actor_language::LocalUserLanguage, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, @@ -13,16 +13,19 @@ use crate::{ now, DbPool, }, + CommunityVisibility, }; use bcrypt::{hash, DEFAULT_COST}; use diesel::{ dsl::{insert_into, not, IntervalDsl}, result::Error, + CombineDsl, ExpressionMethods, JoinOnDsl, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl LocalUser { pub async fn create( @@ -112,11 +115,11 @@ impl LocalUser { let conn = &mut get_conn(pool).await?; // Make sure: - // - The deny reason exists + // - An admin has interacted with the application // - The app is older than a week // - The accepted_application is false let old_denied_registrations = registration_application::table - .filter(registration_application::deny_reason.is_not_null()) + .filter(registration_application::admin_id.is_not_null()) .filter(registration_application::published.lt(now() - 1.week())) .select(registration_application::local_user_id); @@ -215,6 +218,72 @@ impl LocalUser { blocked_instances, }) } + + /// Checks to make sure the acting admin is higher than the target admin + pub async fn is_higher_admin_check( + pool: &mut DbPool<'_>, + admin_person_id: PersonId, + target_person_ids: Vec, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + + // Build the list of persons + let mut persons = target_person_ids; + persons.push(admin_person_id); + persons.dedup(); + + let res = local_user::table + .filter(local_user::admin.eq(true)) + .filter(local_user::person_id.eq_any(persons)) + .order_by(local_user::id) + // This does a limit 1 select first + .first::(conn) + .await?; + + // If the first result sorted by published is the acting admin + if res.person_id == admin_person_id { + Ok(()) + } else { + Err(LemmyErrorType::NotHigherAdmin)? + } + } + + /// Checks to make sure the acting moderator is higher than the target moderator + pub async fn is_higher_mod_or_admin_check( + pool: &mut DbPool<'_>, + for_community_id: CommunityId, + admin_person_id: PersonId, + target_person_ids: Vec, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + + // Build the list of persons + let mut persons = target_person_ids; + persons.push(admin_person_id); + persons.dedup(); + + let admins = local_user::table + .filter(local_user::admin.eq(true)) + .filter(local_user::person_id.eq_any(&persons)) + .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 res = admins.union_all(mods).get_results::(conn).await?; + let first_person = res.as_slice().first().ok_or(LemmyErrorType::NotHigherMod)?; + + // If the first result sorted by published is the acting mod + if *first_person == admin_person_id { + Ok(()) + } else { + Err(LemmyErrorType::NotHigherMod)? + } + } } /// Adds some helper functions for an optional LocalUser @@ -225,6 +294,12 @@ pub trait LocalUserOptionHelper { fn show_read_posts(&self) -> bool; fn is_admin(&self) -> bool; fn show_nsfw(&self, site: &Site) -> bool; + fn visible_communities_only(&self, query: Q) -> Q + where + Q: diesel::query_dsl::methods::FilterDsl< + diesel::dsl::Eq, + Output = Q, + >; } impl LocalUserOptionHelper for Option<&LocalUser> { @@ -253,14 +328,32 @@ impl LocalUserOptionHelper for Option<&LocalUser> { .map(|l| l.show_nsfw) .unwrap_or(site.content_warning.is_some()) } + + fn visible_communities_only(&self, query: Q) -> Q + where + Q: diesel::query_dsl::methods::FilterDsl< + diesel::dsl::Eq, + Output = Q, + >, + { + if self.is_none() { + query.filter(community::visibility.eq(CommunityVisibility::Public)) + } else { + query + } + } } impl LocalUserInsertForm { pub fn test_form(person_id: PersonId) -> Self { - Self::builder() - .person_id(person_id) - .password_encrypted(String::new()) - .build() + Self::new(person_id, String::new()) + } + + pub fn test_form_admin(person_id: PersonId) -> Self { + LocalUserInsertForm { + admin: Some(true), + ..Self::test_form(person_id) + } } } @@ -272,3 +365,58 @@ pub struct UserBackupLists { pub blocked_users: Vec, pub blocked_instances: Vec, } + +#[cfg(test)] +#[allow(clippy::indexing_slicing)] +mod tests { + use crate::{ + source::{ + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + }, + traits::Crud, + utils::build_db_pool_for_tests, + }; + use lemmy_utils::error::LemmyResult; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn test_admin_higher_check() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let fiona_person = PersonInsertForm::test_form(inserted_instance.id, "fiona"); + let inserted_fiona_person = Person::create(pool, &fiona_person).await?; + + let fiona_local_user_form = LocalUserInsertForm::test_form_admin(inserted_fiona_person.id); + let _inserted_fiona_local_user = + LocalUser::create(pool, &fiona_local_user_form, vec![]).await?; + + let delores_person = PersonInsertForm::test_form(inserted_instance.id, "delores"); + let inserted_delores_person = Person::create(pool, &delores_person).await?; + let delores_local_user_form = LocalUserInsertForm::test_form_admin(inserted_delores_person.id); + let _inserted_delores_local_user = + LocalUser::create(pool, &delores_local_user_form, vec![]).await?; + + let admin_person_ids = vec![inserted_fiona_person.id, inserted_delores_person.id]; + + // Make sure fiona is marked as a higher admin than delores, and vice versa + let fiona_higher_check = + LocalUser::is_higher_admin_check(pool, inserted_fiona_person.id, admin_person_ids.clone()) + .await; + assert!(fiona_higher_check.is_ok()); + + // This should throw an error, since delores was added later + let delores_higher_check = + LocalUser::is_higher_admin_check(pool, inserted_delores_person.id, admin_person_ids).await; + assert!(delores_higher_check.is_err()); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } +} diff --git a/crates/db_schema/src/impls/local_user_vote_display_mode.rs b/crates/db_schema/src/impls/local_user_vote_display_mode.rs index d77502335..2d169f81b 100644 --- a/crates/db_schema/src/impls/local_user_vote_display_mode.rs +++ b/crates/db_schema/src/impls/local_user_vote_display_mode.rs @@ -31,6 +31,7 @@ impl LocalUserVoteDisplayMode { .get_result::(conn) .await } + pub async fn update( pool: &mut DbPool<'_>, local_user_id: LocalUserId, diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index 0b1351af1..be05ed8ac 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -72,10 +72,7 @@ mod tests { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy prw"); let inserted_person = Person::create(pool, &new_person).await?; - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("pass".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; // Create password reset token diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index f318a503a..f2909218c 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -182,23 +182,24 @@ impl ApubActor for Person { impl Followable for PersonFollower { type Form = PersonFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { - use crate::schema::person_follower::dsl::{follower_id, person_follower, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(person_follower) + insert_into(person_follower::table) .values(form) - .on_conflict((follower_id, person_id)) + .on_conflict((person_follower::follower_id, person_follower::person_id)) .do_update() .set(form) .get_result::(conn) .await } + + /// Currently no user following async fn follow_accepted(_: &mut DbPool<'_>, _: CommunityId, _: PersonId) -> Result { - unimplemented!() + Err(Error::NotFound) } + async fn unfollow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { - use crate::schema::person_follower::dsl::person_follower; let conn = &mut get_conn(pool).await?; - diesel::delete(person_follower.find((form.follower_id, form.person_id))) + diesel::delete(person_follower::table.find((form.follower_id, form.person_id))) .execute(conn) .await } @@ -220,7 +221,6 @@ impl PersonFollower { } #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { @@ -232,22 +232,21 @@ mod tests { traits::{Crud, Followable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; 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, "holly"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let expected_person = Person { id: inserted_person.id, @@ -274,57 +273,54 @@ mod tests { }; let read_person = Person::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let update_person_form = PersonUpdateForm { actor_id: Some(inserted_person.actor_id.clone()), ..Default::default() }; - let updated_person = Person::update(pool, inserted_person.id, &update_person_form) - .await - .unwrap(); + let updated_person = Person::update(pool, inserted_person.id, &update_person_form).await?; - let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let num_deleted = Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_person, read_person); assert_eq!(expected_person, inserted_person); assert_eq!(expected_person, updated_person); assert_eq!(1, num_deleted); + + Ok(()) } #[tokio::test] #[serial] - async fn follow() { + async fn follow() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; 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 person_form_1 = PersonInsertForm::test_form(inserted_instance.id, "erich"); - let person_1 = Person::create(pool, &person_form_1).await.unwrap(); + let person_1 = Person::create(pool, &person_form_1).await?; let person_form_2 = PersonInsertForm::test_form(inserted_instance.id, "michele"); - let person_2 = Person::create(pool, &person_form_2).await.unwrap(); + let person_2 = Person::create(pool, &person_form_2).await?; let follow_form = PersonFollowerForm { person_id: person_1.id, follower_id: person_2.id, pending: false, }; - let person_follower = PersonFollower::follow(pool, &follow_form).await.unwrap(); + let person_follower = PersonFollower::follow(pool, &follow_form).await?; assert_eq!(person_1.id, person_follower.person_id); assert_eq!(person_2.id, person_follower.follower_id); assert!(!person_follower.pending); - let followers = PersonFollower::list_followers(pool, person_1.id) - .await - .unwrap(); + let followers = PersonFollower::list_followers(pool, person_1.id).await?; assert_eq!(vec![person_2], followers); - let unfollow = PersonFollower::unfollow(pool, &follow_form).await.unwrap(); + let unfollow = PersonFollower::unfollow(pool, &follow_form).await?; assert_eq!(1, unfollow); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index ac6cf76aa..8e14bee9f 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -390,6 +390,7 @@ mod tests { use pretty_assertions::assert_eq; use serial_test::serial; use std::collections::HashSet; + use url::Url; #[tokio::test] #[serial] @@ -447,7 +448,9 @@ mod tests { embed_description: None, embed_video_url: None, thumbnail_url: None, - ap_id: inserted_post.ap_id.clone(), + ap_id: Url::parse(&format!("https://lemmy-alpha/post/{}", inserted_post.id)) + .unwrap() + .into(), local: true, language_id: Default::default(), featured_community: false, diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index 3cbfd052d..fe3629a1a 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -100,6 +100,7 @@ mod tests { }; use pretty_assertions::assert_eq; use serial_test::serial; + use url::Url; #[tokio::test] #[serial] @@ -138,7 +139,12 @@ mod tests { read: false, updated: None, published: inserted_private_message.published, - ap_id: inserted_private_message.ap_id.clone(), + ap_id: Url::parse(&format!( + "https://lemmy-alpha/private_message/{}", + inserted_private_message.id + )) + .unwrap() + .into(), local: true, }; diff --git a/crates/db_schema/src/impls/private_message_report.rs b/crates/db_schema/src/impls/private_message_report.rs index b5d8fd039..0d5876659 100644 --- a/crates/db_schema/src/impls/private_message_report.rs +++ b/crates/db_schema/src/impls/private_message_report.rs @@ -52,7 +52,7 @@ impl Reportable for PrivateMessageReport { _pm_id_: PrivateMessageId, _by_resolver_id: PersonId, ) -> Result { - unimplemented!() + Err(Error::NotFound) } async fn unresolve( diff --git a/crates/db_schema/src/impls/registration_application.rs b/crates/db_schema/src/impls/registration_application.rs index 46b7d4bee..055ffb51f 100644 --- a/crates/db_schema/src/impls/registration_application.rs +++ b/crates/db_schema/src/impls/registration_application.rs @@ -1,6 +1,6 @@ use crate::{ diesel::OptionalExtension, - newtypes::LocalUserId, + newtypes::{LocalUserId, RegistrationApplicationId}, schema::registration_application::dsl::{local_user_id, registration_application}, source::registration_application::{ RegistrationApplication, @@ -17,7 +17,7 @@ use diesel_async::RunQueryDsl; impl Crud for RegistrationApplication { type InsertForm = RegistrationApplicationInsertForm; type UpdateForm = RegistrationApplicationUpdateForm; - type IdType = i32; + type IdType = RegistrationApplicationId; async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; diff --git a/crates/db_schema/src/impls/site.rs b/crates/db_schema/src/impls/site.rs index a371f9e07..9dbd2401d 100644 --- a/crates/db_schema/src/impls/site.rs +++ b/crates/db_schema/src/impls/site.rs @@ -20,7 +20,7 @@ impl Crud for Site { /// Use SiteView::read_local, or Site::read_from_apub_id instead async fn read(_pool: &mut DbPool<'_>, _site_id: SiteId) -> Result, Error> { - unimplemented!() + Err(Error::NotFound) } async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 431243221..c29ec6443 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -48,7 +48,7 @@ pub mod utils; mod schema_setup; use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumString}; +use strum::{Display, EnumString}; #[cfg(feature = "full")] use ts_rs::TS; diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index f13ef118d..c715305bb 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -85,12 +85,6 @@ impl fmt::Display for PrivateMessageId { /// The person mention id. pub struct PersonMentionId(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 person block id. -pub struct PersonBlockId(i32); - #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] @@ -154,6 +148,12 @@ 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 registration application id. +pub struct RegistrationApplicationId(i32); + #[cfg(feature = "full")] #[derive(Serialize, Deserialize)] #[serde(remote = "Ltree")] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 50bdac751..fc418ec28 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -309,6 +309,15 @@ diesel::table! { } } +diesel::table! { + image_details (link) { + link -> Text, + width -> Int4, + height -> Int4, + content_type -> Text, + } +} + diesel::table! { instance (id) { id -> Int4, @@ -702,7 +711,7 @@ diesel::table! { id -> Int4, #[max_length = 200] name -> Varchar, - #[max_length = 512] + #[max_length = 2000] url -> Nullable, body -> Nullable, creator_id -> Int4, @@ -849,8 +858,7 @@ diesel::table! { } diesel::table! { - remote_image (id) { - id -> Int4, + remote_image (link) { link -> Text, published -> Timestamptz, } @@ -1055,6 +1063,7 @@ diesel::allow_tables_to_appear_in_same_query!( federation_allowlist, federation_blocklist, federation_queue_state, + image_details, instance, instance_block, language, diff --git a/crates/db_schema/src/source/images.rs b/crates/db_schema/src/source/images.rs index 9d48e011b..0dea4b84f 100644 --- a/crates/db_schema/src/source/images.rs +++ b/crates/db_schema/src/source/images.rs @@ -1,13 +1,12 @@ use crate::newtypes::{DbUrl, LocalUserId}; #[cfg(feature = "full")] -use crate::schema::{local_image, remote_image}; +use crate::schema::{image_details, local_image, remote_image}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; 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)] @@ -30,7 +29,7 @@ pub struct LocalImage { pub published: DateTime, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = local_image))] pub struct LocalImageForm { @@ -46,15 +45,39 @@ pub struct LocalImageForm { #[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable))] #[cfg_attr(feature = "full", diesel(table_name = remote_image))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", diesel(primary_key(link)))] pub struct RemoteImage { - pub id: i32, pub link: DbUrl, pub published: DateTime, } -#[derive(Debug, Clone, TypedBuilder)] +#[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))] +#[cfg_attr(feature = "full", ts(export))] +#[cfg_attr(feature = "full", diesel(table_name = image_details))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", diesel(primary_key(link)))] +pub struct ImageDetails { + pub link: DbUrl, + pub width: i32, + pub height: i32, + pub content_type: String, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = image_details))] +pub struct ImageDetailsForm { + pub link: DbUrl, + pub width: i32, + pub height: i32, + pub content_type: String, +} diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 5d592ddf1..c7a5b5224 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -11,7 +11,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)] @@ -69,38 +68,59 @@ pub struct LocalUser { pub collapse_bot_comments: 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))] pub struct LocalUserInsertForm { - #[builder(!default)] pub person_id: PersonId, - #[builder(!default)] pub password_encrypted: String, + #[new(default)] pub email: Option, + #[new(default)] pub show_nsfw: Option, + #[new(default)] pub theme: Option, + #[new(default)] pub default_sort_type: Option, + #[new(default)] pub default_listing_type: Option, + #[new(default)] pub interface_language: Option, + #[new(default)] pub show_avatars: Option, + #[new(default)] pub send_notifications_to_email: Option, + #[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, + #[new(default)] pub accepted_application: Option, + #[new(default)] pub totp_2fa_secret: Option>, + #[new(default)] pub open_links_in_new_tab: Option, + #[new(default)] pub blur_nsfw: Option, + #[new(default)] pub auto_expand: Option, + #[new(default)] pub infinite_scroll_enabled: Option, + #[new(default)] pub admin: Option, + #[new(default)] pub post_listing_mode: Option, + #[new(default)] pub totp_2fa_enabled: Option, + #[new(default)] pub enable_keyboard_navigation: Option, + #[new(default)] pub enable_animated_images: Option, + #[new(default)] pub collapse_bot_comments: Option, } diff --git a/crates/db_schema/src/source/registration_application.rs b/crates/db_schema/src/source/registration_application.rs index 2824951d7..2ac973f34 100644 --- a/crates/db_schema/src/source/registration_application.rs +++ b/crates/db_schema/src/source/registration_application.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{LocalUserId, PersonId}; +use crate::newtypes::{LocalUserId, PersonId, RegistrationApplicationId}; #[cfg(feature = "full")] use crate::schema::registration_application; use chrono::{DateTime, Utc}; @@ -15,7 +15,7 @@ use ts_rs::TS; #[cfg_attr(feature = "full", ts(export))] /// A registration application. pub struct RegistrationApplication { - pub id: i32, + pub id: RegistrationApplicationId, pub local_user_id: LocalUserId, pub answer: String, pub admin_id: Option, diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 6761adeee..b71c43495 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,15 +1,7 @@ -use crate::{ - diesel::ExpressionMethods, - newtypes::{DbUrl, PersonId}, - schema::community, - CommentSortType, - CommunityVisibility, - SortType, -}; +use crate::{newtypes::DbUrl, CommentSortType, SortType}; use chrono::{DateTime, TimeDelta, Utc}; use deadpool::Runtime; use diesel::{ - dsl, helper_types::AsExprOf, pg::Pg, query_builder::{Query, QueryFragment}, @@ -30,7 +22,8 @@ use diesel_async::{ AsyncDieselConnectionManager, ManagerConfig, }, - SimpleAsyncConnection, + AsyncConnection, + RunQueryDsl, }; use futures_util::{future::BoxFuture, Future, FutureExt}; use i_love_jesus::CursorKey; @@ -39,7 +32,6 @@ use lemmy_utils::{ settings::SETTINGS, utils::validation::clean_url_params, }; -use once_cell::sync::Lazy; use regex::Regex; use rustls::{ client::danger::{ @@ -56,7 +48,7 @@ use rustls::{ }; use std::{ ops::{Deref, DerefMut}, - sync::Arc, + sync::{Arc, LazyLock}, time::Duration, }; use tracing::error; @@ -332,34 +324,46 @@ pub fn diesel_url_create(opt: Option<&str>) -> LemmyResult> { fn establish_connection(config: &str) -> BoxFuture> { let fut = async { - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install rustls crypto provider"); - - let rustls_config = DangerousClientConfigBuilder { - cfg: ClientConfig::builder(), - } - .with_custom_certificate_verifier(Arc::new(NoCertVerifier {})) - .with_no_client_auth(); - - let tls = tokio_postgres_rustls::MakeRustlsConnect::new(rustls_config); - let (client, conn) = tokio_postgres::connect(config, tls) - .await - .map_err(|e| ConnectionError::BadConnection(e.to_string()))?; - tokio::spawn(async move { - if let Err(e) = conn.await { - error!("Database connection failed: {e}"); + // We only support TLS with sslmode=require currently + let mut conn = if config.contains("sslmode=require") { + let rustls_config = DangerousClientConfigBuilder { + cfg: ClientConfig::builder(), } - }); - let mut conn = AsyncPgConnection::try_from(client).await?; - // * Change geqo_threshold back to default value if it was changed, so it's higher than the - // collapse limits - // * Change collapse limits from 8 to 11 so the query planner can find a better table join order - // for more complicated queries - conn - .batch_execute("SET geqo_threshold=12;SET from_collapse_limit=11;SET join_collapse_limit=11;") - .await - .map_err(ConnectionError::CouldntSetupConfiguration)?; + .with_custom_certificate_verifier(Arc::new(NoCertVerifier {})) + .with_no_client_auth(); + + let tls = tokio_postgres_rustls::MakeRustlsConnect::new(rustls_config); + let (client, conn) = tokio_postgres::connect(config, tls) + .await + .map_err(|e| ConnectionError::BadConnection(e.to_string()))?; + tokio::spawn(async move { + if let Err(e) = conn.await { + error!("Database connection failed: {e}"); + } + }); + AsyncPgConnection::try_from(client).await? + } else { + AsyncPgConnection::establish(config).await? + }; + + diesel::select(( + // Change geqo_threshold back to default value if it was changed, so it's higher than the + // collapse limits + functions::set_config("geqo_threshold", "12", false), + // Change collapse limits from 8 to 11 so the query planner can find a better table join + // order for more complicated queries + functions::set_config("from_collapse_limit", "11", false), + functions::set_config("join_collapse_limit", "11", false), + // Set `lemmy.protocol_and_hostname` so triggers can use it + functions::set_config( + "lemmy.protocol_and_hostname", + SETTINGS.get_protocol_and_hostname(), + false, + ), + )) + .execute(&mut conn) + .await + .map_err(ConnectionError::CouldntSetupConfiguration)?; Ok(conn) }; fut.boxed() @@ -418,17 +422,11 @@ impl ServerCertVerifier for NoCertVerifier { pub async fn build_db_pool() -> LemmyResult { let db_url = SETTINGS.get_database_url(); - // We only support TLS with sslmode=require currently - let tls_enabled = db_url.contains("sslmode=require"); - let manager = if tls_enabled { - // 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 - let mut config = ManagerConfig::default(); - config.custom_setup = Box::new(establish_connection); - AsyncDieselConnectionManager::::new_with_config(&db_url, config) - } else { - AsyncDieselConnectionManager::::new(&db_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 + let mut config = ManagerConfig::default(); + config.custom_setup = Box::new(establish_connection); + let manager = AsyncDieselConnectionManager::::new_with_config(&db_url, config); let pool = Pool::builder(manager) .max_size(SETTINGS.database.pool_size) .runtime(Runtime::Tokio1) @@ -479,13 +477,13 @@ pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType { } } -static EMAIL_REGEX: Lazy = Lazy::new(|| { +static EMAIL_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$") .expect("compile email regex") }); pub mod functions { - use diesel::sql_types::{BigInt, Text, Timestamptz}; + use diesel::sql_types::{BigInt, Bool, Text, Timestamptz}; sql_function! { #[sql_name = "r.hot_rank"] @@ -508,6 +506,8 @@ 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*"; @@ -579,20 +579,6 @@ impl Queries { } } -pub fn visible_communities_only(my_person_id: Option, query: Q) -> Q -where - Q: diesel::query_dsl::methods::FilterDsl< - dsl::Eq, - Output = Q, - >, -{ - if my_person_id.is_none() { - query.filter(community::visibility.eq(CommunityVisibility::Public)) - } else { - query - } -} - #[cfg(test)] #[allow(clippy::indexing_slicing)] mod tests { diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 950d061ba..1866aaee9 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -196,7 +196,7 @@ impl CommentReportView { 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, @@ -301,10 +301,7 @@ mod tests { let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_timmy.id) - .password_encrypted("123".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) .await .unwrap(); diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 61dbceb4b..e2752a0c7 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -36,22 +36,13 @@ use lemmy_db_schema::{ post, }, source::local_user::LocalUser, - utils::{ - fuzzy_search, - limit_and_offset, - visible_communities_only, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, ListingType, }; fn queries<'a>() -> Queries< - impl ReadFn<'a, CommentView, (CommentId, Option)>, + impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>, impl ListFn<'a, CommentView, CommentQuery<'a>>, > { let is_creator_banned_from_community = exists( @@ -182,9 +173,12 @@ fn queries<'a>() -> Queries< }; let read = move |mut conn: DbConn<'a>, - (comment_id, my_person_id): (CommentId, Option)| async move { - let mut query = all_joins(comment::table.find(comment_id).into_boxed(), my_person_id); - query = visible_communities_only(my_person_id, query); + (comment_id, my_local_user): (CommentId, Option<&'a LocalUser>)| async move { + let mut query = all_joins( + comment::table.find(comment_id).into_boxed(), + my_local_user.person_id(), + ); + query = my_local_user.visible_communities_only(query); query.first(&mut conn).await }; @@ -252,7 +246,7 @@ fn queries<'a>() -> Queries< } // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only { + if options.saved_only.unwrap_or_default() { query = query .filter(comment_saved::person_id.is_not_null()) .then_order_by(comment_saved::published.desc()); @@ -260,9 +254,9 @@ fn queries<'a>() -> Queries< if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); - if options.liked_only { + if options.liked_only.unwrap_or_default() { query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); - } else if options.disliked_only { + } else if options.disliked_only.unwrap_or_default() { query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); } } @@ -301,7 +295,7 @@ fn queries<'a>() -> Queries< query = query.filter(not(is_creator_blocked(person_id_join))); }; - query = visible_communities_only(options.local_user.person_id(), query); + query = options.local_user.visible_communities_only(query); // A Max depth given means its a tree fetch let (limit, offset) = if let Some(max_depth) = options.max_depth { @@ -366,16 +360,16 @@ fn queries<'a>() -> Queries< } impl CommentView { - pub async fn read( + pub async fn read<'a>( pool: &mut DbPool<'_>, comment_id: CommentId, - my_person_id: Option, + my_local_user: Option<&'a LocalUser>, ) -> Result, Error> { // 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_person_id)).await { + if let Ok(Some(res)) = queries().read(pool, (comment_id, my_local_user)).await { let mut new_view = res.clone(); - if my_person_id.is_some() && res.my_vote.is_none() { + if my_local_user.is_some() && res.my_vote.is_none() { new_view.my_vote = Some(0); } if res.comment.deleted || res.comment.removed { @@ -398,9 +392,9 @@ pub struct CommentQuery<'a> { pub creator_id: Option, pub local_user: Option<&'a LocalUser>, pub search_term: Option, - pub saved_only: bool, - pub liked_only: bool, - pub disliked_only: bool, + pub saved_only: Option, + pub liked_only: Option, + pub disliked_only: Option, pub page: Option, pub limit: Option, pub max_depth: Option, @@ -490,11 +484,8 @@ mod tests { let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy"); let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?; - let timmy_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_timmy_person.id) - .admin(Some(true)) - .password_encrypted(String::new()) - .build(); + 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?; let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara"); @@ -676,7 +667,7 @@ mod tests { let read_comment_from_blocked_person = CommentView::read( pool, data.inserted_comment_1.id, - Some(data.timmy_local_user_view.person.id), + Some(&data.timmy_local_user_view.local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; @@ -711,8 +702,8 @@ mod tests { CommentLike::like(pool, &comment_like_form).await?; let read_liked_comment_views = CommentQuery { - local_user: (Some(&data.timmy_local_user_view.local_user)), - liked_only: (true), + local_user: Some(&data.timmy_local_user_view.local_user), + liked_only: Some(true), ..Default::default() } .list(pool) @@ -727,8 +718,8 @@ mod tests { assert_length!(1, read_liked_comment_views); let read_disliked_comment_views: Vec = CommentQuery { - local_user: (Some(&data.timmy_local_user_view.local_user)), - disliked_only: (true), + local_user: Some(&data.timmy_local_user_view.local_user), + disliked_only: Some(true), ..Default::default() } .list(pool) @@ -980,7 +971,7 @@ mod tests { // Fetch the saved comments let comments = CommentQuery { local_user: Some(&data.timmy_local_user_view.local_user), - saved_only: true, + saved_only: Some(true), ..Default::default() } .list(pool) @@ -1171,7 +1162,7 @@ mod tests { let authenticated_comment = CommentView::read( pool, data.inserted_comment_0.id, - Some(data.timmy_local_user_view.person.id), + Some(&data.timmy_local_user_view.local_user), ) .await; assert!(authenticated_comment.is_ok()); @@ -1211,7 +1202,7 @@ mod tests { let comment_view = CommentView::read( pool, data.inserted_comment_0.id, - Some(inserted_banned_from_comm_local_user.person_id), + Some(&inserted_banned_from_comm_local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; @@ -1232,7 +1223,7 @@ mod tests { let comment_view = CommentView::read( pool, data.inserted_comment_0.id, - Some(data.timmy_local_user_view.person.id), + Some(&data.timmy_local_user_view.local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index e89b7d545..0cd06dd4e 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -323,10 +323,7 @@ mod tests { let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_timmy.id) - .password_encrypted("123".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) .await .unwrap(); diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index fb616b0d3..0ec7e0a5d 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -28,6 +28,7 @@ use lemmy_db_schema::{ community_follower, community_moderator, community_person_ban, + image_details, instance_block, local_user, local_user_language, @@ -48,7 +49,6 @@ use lemmy_db_schema::{ get_conn, limit_and_offset, now, - visible_communities_only, Commented, DbConn, DbPool, @@ -63,7 +63,7 @@ use lemmy_db_schema::{ use tracing::debug; fn queries<'a>() -> Queries< - impl ReadFn<'a, PostView, (PostId, Option, bool)>, + impl ReadFn<'a, PostView, (PostId, Option<&'a LocalUser>, bool)>, impl ListFn<'a, PostView, (PostQuery<'a>, &'a Site)>, > { let is_creator_banned_from_community = exists( @@ -141,6 +141,7 @@ fn queries<'a>() -> Queries< .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< @@ -218,6 +219,7 @@ fn queries<'a>() -> Queries< .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 @@ -229,6 +231,7 @@ fn queries<'a>() -> Queries< 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, @@ -247,52 +250,56 @@ fn queries<'a>() -> Queries< )) }; - let read = - move |mut conn: DbConn<'a>, - (post_id, my_person_id, is_mod_or_admin): (PostId, Option, bool)| async move { - // The left join below will return None in this case - let person_id_join = my_person_id.unwrap_or(PersonId(-1)); + let read = move |mut conn: DbConn<'a>, + (post_id, my_local_user, is_mod_or_admin): ( + PostId, + Option<&'a LocalUser>, + bool, + )| async move { + // The left join below will return None in this case + let my_person_id = my_local_user.person_id(); + let person_id_join = my_person_id.unwrap_or(PersonId(-1)); - let mut query = all_joins( - post_aggregates::table - .filter(post_aggregates::post_id.eq(post_id)) - .into_boxed(), - my_person_id, - ); + let mut query = all_joins( + post_aggregates::table + .filter(post_aggregates::post_id.eq(post_id)) + .into_boxed(), + my_person_id, + ); - // Hide deleted and removed for non-admins or mods - if !is_mod_or_admin { - query = query - .filter( - community::removed - .eq(false) - .or(post::creator_id.eq(person_id_join)), - ) - .filter( - post::removed - .eq(false) - .or(post::creator_id.eq(person_id_join)), - ) - // users can see their own deleted posts - .filter( - community::deleted - .eq(false) - .or(post::creator_id.eq(person_id_join)), - ) - .filter( - post::deleted - .eq(false) - .or(post::creator_id.eq(person_id_join)), - ); - } + // Hide deleted and removed for non-admins or mods + if !is_mod_or_admin { + query = query + .filter( + community::removed + .eq(false) + .or(post::creator_id.eq(person_id_join)), + ) + .filter( + post::removed + .eq(false) + .or(post::creator_id.eq(person_id_join)), + ) + // users can see their own deleted posts + .filter( + community::deleted + .eq(false) + .or(post::creator_id.eq(person_id_join)), + ) + .filter( + post::deleted + .eq(false) + .or(post::creator_id.eq(person_id_join)), + ); + } - query = visible_communities_only(my_person_id, query); + query = my_local_user.visible_communities_only(query); - Commented::new(query) - .text("PostView::read") - .first(&mut conn) - .await - }; + Commented::new(query) + .text("PostView::read") + .first(&mut conn) + .await + }; 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 @@ -389,7 +396,10 @@ fn queries<'a>() -> Queries< .filter(not(post::removed.or(post::deleted))); } - if !options.local_user.show_nsfw(site) { + if !options + .show_nsfw + .unwrap_or(options.local_user.show_nsfw(site)) + { query = query .filter(post::nsfw.eq(false)) .filter(community::nsfw.eq(false)); @@ -400,14 +410,17 @@ fn queries<'a>() -> Queries< }; // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only { + if options.saved_only.unwrap_or_default() { query = query .filter(post_saved::person_id.is_not_null()) .then_order_by(post_saved::published.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. - else if !options.local_user.show_read_posts() { + else if !options + .show_read + .unwrap_or(options.local_user.show_read_posts()) + { // 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()) { @@ -415,7 +428,7 @@ fn queries<'a>() -> Queries< } } - if !options.show_hidden { + 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))); @@ -424,14 +437,14 @@ fn queries<'a>() -> Queries< 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 { + if options.liked_only.unwrap_or_default() { query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); - } else if options.disliked_only { + } else if options.disliked_only.unwrap_or_default() { query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); } }; - query = visible_communities_only(options.local_user.person_id(), query); + query = options.local_user.visible_communities_only(query); // Dont filter blocks or missing languages for moderator view type if let (Some(person_id), false) = ( @@ -473,7 +486,7 @@ fn queries<'a>() -> Queries< let page_after = options.page_after.map(|c| c.0); let page_before_or_equal = options.page_before_or_equal.map(|c| c.0); - if options.page_back { + if options.page_back.unwrap_or_default() { query = query .before(page_after) .after_or_equal(page_before_or_equal) @@ -546,14 +559,14 @@ fn queries<'a>() -> Queries< } impl PostView { - pub async fn read( + pub async fn read<'a>( pool: &mut DbPool<'_>, post_id: PostId, - my_person_id: Option, + my_local_user: Option<&'a LocalUser>, is_mod_or_admin: bool, ) -> Result, Error> { queries() - .read(pool, (post_id, my_person_id, is_mod_or_admin)) + .read(pool, (post_id, my_local_user, is_mod_or_admin)) .await } } @@ -601,15 +614,17 @@ pub struct PostQuery<'a> { pub local_user: Option<&'a LocalUser>, pub search_term: Option, pub url_search: Option, - pub saved_only: bool, - pub liked_only: bool, - pub disliked_only: bool, + pub saved_only: Option, + pub liked_only: Option, + pub disliked_only: Option, pub page: Option, pub limit: Option, pub page_after: Option, pub page_before_or_equal: Option, - pub page_back: bool, - pub show_hidden: bool, + pub page_back: Option, + pub show_hidden: Option, + pub show_read: Option, + pub show_nsfw: Option, } impl<'a> PostQuery<'a> { @@ -680,7 +695,7 @@ impl<'a> PostQuery<'a> { if (v.len() as i64) < limit { Ok(Some(self.clone())) } else { - let item = if self.page_back { + let item = if self.page_back.unwrap_or_default() { // for backward pagination, get first element instead v.into_iter().next() } else { @@ -931,7 +946,7 @@ mod tests { let post_listing_single_with_person = PostView::read( pool, data.inserted_post.id, - Some(data.local_user_view.person.id), + Some(&data.local_user_view.local_user), false, ) .await? @@ -1060,7 +1075,7 @@ mod tests { let post_listing_single_with_person = PostView::read( pool, data.inserted_post.id, - Some(data.local_user_view.person.id), + Some(&data.local_user_view.local_user), false, ) .await? @@ -1119,7 +1134,7 @@ mod tests { // Read the liked only let read_liked_post_listing = PostQuery { community_id: Some(data.inserted_community.id), - liked_only: true, + liked_only: Some(true), ..data.default_post_query() } .list(&data.site, pool) @@ -1130,7 +1145,7 @@ mod tests { let read_disliked_post_listing = PostQuery { community_id: Some(data.inserted_community.id), - disliked_only: true, + disliked_only: Some(true), ..data.default_post_query() } .list(&data.site, pool) @@ -1456,7 +1471,7 @@ mod tests { loop { let post_listings = PostQuery { page_after: page_before, - page_back: true, + page_back: Some(true), ..options.clone() } .list(&data.site, pool) @@ -1514,6 +1529,26 @@ mod tests { let post_listings_hide_read = data.default_post_query().list(&data.site, pool).await?; assert_eq!(vec![POST], names(&post_listings_hide_read)); + // Test with the show_read override as true + let post_listings_show_read_true = PostQuery { + show_read: Some(true), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + assert_eq!( + vec![POST_BY_BOT, POST], + names(&post_listings_show_read_true) + ); + + // Test with the show_read override as false + let post_listings_show_read_false = PostQuery { + show_read: Some(false), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + assert_eq!(vec![POST], names(&post_listings_show_read_false)); cleanup(data, pool).await } @@ -1540,7 +1575,7 @@ mod tests { let post_listings_show_hidden = PostQuery { sort: Some(SortType::New), local_user: Some(&data.local_user_view.local_user), - show_hidden: true, + show_hidden: Some(true), ..Default::default() } .list(&data.site, pool) @@ -1558,6 +1593,48 @@ mod tests { cleanup(data, pool).await } + #[tokio::test] + #[serial] + async fn post_listings_hide_nsfw() -> LemmyResult<()> { + let pool = &build_db_pool().await?; + 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_bot_post.id, &update_form).await?; + + // Make sure you don't see the nsfw post in the regular results + let post_listings_hide_nsfw = data.default_post_query().list(&data.site, pool).await?; + assert_eq!(vec![POST], names(&post_listings_hide_nsfw)); + + // Make sure it does come back with the show_nsfw option + let post_listings_show_nsfw = PostQuery { + sort: Some(SortType::New), + show_nsfw: Some(true), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + 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 + ); + + cleanup(data, pool).await + } + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { let num_deleted = Post::delete(pool, data.inserted_post.id).await?; Community::delete(pool, data.inserted_community.id).await?; @@ -1631,6 +1708,7 @@ mod tests { public_key: inserted_person.public_key.clone(), last_refreshed_at: inserted_person.last_refreshed_at, }, + image_details: None, creator_banned_from_community: false, banned_from_community: false, creator_is_moderator: false, @@ -1727,7 +1805,7 @@ mod tests { let authenticated_post = PostView::read( pool, data.inserted_post.id, - Some(data.local_user_view.person.id), + Some(&data.local_user_view.local_user), false, ) .await; @@ -1769,7 +1847,7 @@ mod tests { let post_view = PostView::read( pool, data.inserted_post.id, - Some(inserted_banned_from_comm_local_user.person_id), + Some(&inserted_banned_from_comm_local_user), false, ) .await? @@ -1791,7 +1869,7 @@ mod tests { let post_view = PostView::read( pool, data.inserted_post.id, - Some(data.local_user_view.person.id), + Some(&data.local_user_view.local_user), false, ) .await? diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index cd63859af..54c7f7598 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -11,12 +11,18 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, + newtypes::{PersonId, RegistrationApplicationId}, schema::{local_user, person, registration_application}, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, }; +enum ReadBy { + Id(RegistrationApplicationId), + Person(PersonId), +} + fn queries<'a>() -> Queries< - impl ReadFn<'a, RegistrationApplicationView, i32>, + impl ReadFn<'a, RegistrationApplicationView, ReadBy>, impl ListFn<'a, RegistrationApplicationView, RegistrationApplicationQuery>, > { let all_joins = |query: registration_application::BoxedQuery<'a, Pg>| { @@ -36,14 +42,15 @@ fn queries<'a>() -> Queries< )) }; - let read = move |mut conn: DbConn<'a>, registration_application_id: i32| async move { - all_joins( - registration_application::table - .find(registration_application_id) - .into_boxed(), - ) - .first(&mut conn) - .await + let read = move |mut conn: DbConn<'a>, search: ReadBy| async move { + let mut query = all_joins(registration_application::table.into_boxed()); + + query = match search { + ReadBy::Id(id) => query.filter(registration_application::id.eq(id)), + ReadBy::Person(person_id) => query.filter(person::id.eq(person_id)), + }; + + query.first(&mut conn).await }; let list = move |mut conn: DbConn<'a>, options: RegistrationApplicationQuery| async move { @@ -76,11 +83,17 @@ fn queries<'a>() -> Queries< impl RegistrationApplicationView { pub async fn read( pool: &mut DbPool<'_>, - registration_application_id: i32, + id: RegistrationApplicationId, ) -> Result, Error> { - queries().read(pool, registration_application_id).await + queries().read(pool, ReadBy::Id(id)).await } + pub async fn read_by_person( + pool: &mut DbPool<'_>, + person_id: PersonId, + ) -> Result, Error> { + queries().read(pool, ReadBy::Person(person_id)).await + } /// Returns the current unread registration_application count pub async fn get_unread_count( pool: &mut DbPool<'_>, @@ -167,11 +180,7 @@ mod tests { let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap(); - let timmy_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_timmy_person.id) - .password_encrypted("nada".to_string()) - .admin(Some(true)) - .build(); + 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 @@ -181,10 +190,7 @@ mod tests { let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap(); - let sara_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_sara_person.id) - .password_encrypted("nada".to_string()) - .build(); + 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 @@ -209,10 +215,7 @@ mod tests { let inserted_jess_person = Person::create(pool, &jess_person_form).await.unwrap(); - let jess_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_jess_person.id) - .password_encrypted("nada".to_string()) - .build(); + 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 diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 350e4cad4..3c219d63f 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -8,7 +8,7 @@ use lemmy_db_schema::{ community::Community, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, - images::LocalImage, + images::{ImageDetails, LocalImage}, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, @@ -131,6 +131,7 @@ pub struct PostView { pub post: Post, pub creator: Person, pub community: Community, + pub image_details: Option, pub creator_banned_from_community: bool, pub banned_from_community: bool, pub creator_is_moderator: bool, diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index d9e6a3352..af139b8b2 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -33,7 +33,6 @@ serde_with = { workspace = true } ts-rs = { workspace = true, optional = true } chrono.workspace = true strum = { workspace = true } -strum_macros = { workspace = true } [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_views_actor/src/community_moderator_view.rs b/crates/db_views_actor/src/community_moderator_view.rs index 808fc0340..f2a59fd9f 100644 --- a/crates/db_views_actor/src/community_moderator_view.rs +++ b/crates/db_views_actor/src/community_moderator_view.rs @@ -2,10 +2,11 @@ use crate::structs::CommunityModeratorView; use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ + impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, schema::{community, community_moderator, person}, + source::local_user::LocalUser, utils::{get_conn, DbPool}, - CommunityVisibility, }; impl CommunityModeratorView { @@ -60,20 +61,28 @@ impl CommunityModeratorView { pub async fn for_person( pool: &mut DbPool<'_>, person_id: PersonId, - is_authenticated: bool, + local_user: Option<&LocalUser>, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let mut query = community_moderator::table .inner_join(community::table) .inner_join(person::table) .filter(community_moderator::person_id.eq(person_id)) - .filter(community::deleted.eq(false)) - .filter(community::removed.eq(false)) .select((community::all_columns, person::all_columns)) .into_boxed(); - if !is_authenticated { - query = query.filter(community::visibility.eq(CommunityVisibility::Public)); + + query = local_user.visible_communities_only(query); + + // only show deleted communities to creator + if Some(person_id) != local_user.person_id() { + query = query.filter(community::deleted.eq(false)); } + + // Show removed communities to admins only + if !local_user.is_admin() { + query = query.filter(community::removed.eq(false)) + } + query.load::(conn).await } @@ -90,7 +99,7 @@ impl CommunityModeratorView { .distinct_on(community_moderator::community_id) .order_by(( community_moderator::community_id, - community_moderator::person_id, + community_moderator::published, )) .load::(conn) .await diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 25e76c7b3..0e731878a 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -22,27 +22,18 @@ use lemmy_db_schema::{ instance_block, }, source::{community::CommunityFollower, local_user::LocalUser, site::Site}, - utils::{ - fuzzy_search, - limit_and_offset, - visible_communities_only, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, ListingType, SortType, }; fn queries<'a>() -> Queries< - impl ReadFn<'a, CommunityView, (CommunityId, Option, bool)>, + 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_person_id: Option| { + 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_person_id.unwrap_or(PersonId(-1)); + let person_id_join = my_local_user.person_id().unwrap_or(PersonId(-1)); query .inner_join(community_aggregates::table) @@ -89,14 +80,14 @@ fn queries<'a>() -> Queries< .and(community::deleted.eq(false)); let read = move |mut conn: DbConn<'a>, - (community_id, my_person_id, is_mod_or_admin): ( + (community_id, my_local_user, is_mod_or_admin): ( CommunityId, - Option, + Option<&'a LocalUser>, bool, )| async move { let mut query = all_joins( community::table.find(community_id).into_boxed(), - my_person_id, + my_local_user, ) .select(selection); @@ -105,7 +96,7 @@ fn queries<'a>() -> Queries< query = query.filter(not_removed_or_deleted); } - query = visible_communities_only(my_person_id, query); + query = my_local_user.visible_communities_only(query); query.first(&mut conn).await }; @@ -116,11 +107,7 @@ fn queries<'a>() -> Queries< // The left join below will return None in this case let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); - let mut query = all_joins( - community::table.into_boxed(), - options.local_user.person_id(), - ) - .select(selection); + 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); @@ -173,7 +160,7 @@ fn queries<'a>() -> Queries< query = query.filter(community::nsfw.eq(false)); } - query = visible_communities_only(options.local_user.person_id(), query); + query = options.local_user.visible_communities_only(query); let (limit, offset) = limit_and_offset(options.page, options.limit)?; query @@ -187,14 +174,14 @@ fn queries<'a>() -> Queries< } impl CommunityView { - pub async fn read( + pub async fn read<'a>( pool: &mut DbPool<'_>, community_id: CommunityId, - my_person_id: Option, + my_local_user: Option<&'a LocalUser>, is_mod_or_admin: bool, ) -> Result, Error> { queries() - .read(pool, (community_id, my_person_id, is_mod_or_admin)) + .read(pool, (community_id, my_local_user, is_mod_or_admin)) .await } @@ -288,10 +275,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted(String::new()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await .unwrap(); @@ -388,7 +372,7 @@ mod tests { let authenticated_community = CommunityView::read( pool, data.inserted_community.id, - Some(data.local_user.person_id), + Some(&data.local_user), false, ) .await; diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 98a0ca38d..7a2edfb44 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ SortType, }; use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumString}; +use strum::{Display, EnumString}; enum ListMode { Admins, @@ -196,10 +196,7 @@ mod tests { ..PersonInsertForm::test_form(inserted_instance.id, "alice") }; let alice = Person::create(pool, &alice_form).await?; - let alice_local_user_form = LocalUserInsertForm::builder() - .person_id(alice.id) - .password_encrypted(String::new()) - .build(); + let alice_local_user_form = LocalUserInsertForm::test_form(alice.id); let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?; let bob_form = PersonInsertForm { @@ -208,10 +205,7 @@ mod tests { ..PersonInsertForm::test_form(inserted_instance.id, "bob") }; let bob = Person::create(pool, &bob_form).await?; - let bob_local_user_form = LocalUserInsertForm::builder() - .person_id(bob.id) - .password_encrypted(String::new()) - .build(); + let bob_local_user_form = LocalUserInsertForm::test_form(bob.id); let bob_local_user = LocalUser::create(pool, &bob_local_user_form, vec![]).await?; Ok(Data { diff --git a/crates/federate/Cargo.toml b/crates/federate/Cargo.toml index b8b438901..6b76dbf97 100644 --- a/crates/federate/Cargo.toml +++ b/crates/federate/Cargo.toml @@ -27,7 +27,6 @@ futures.workspace = true chrono.workspace = true diesel = { workspace = true, features = ["postgres", "chrono", "serde_json"] } diesel-async = { workspace = true, features = ["deadpool", "postgres"] } -once_cell.workspace = true reqwest.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["full"] } @@ -43,4 +42,4 @@ actix-web.workspace = true tracing-test = "0.2.5" uuid.workspace = true test-context = "0.3.0" -mockall = "0.12.1" +mockall = "0.13.0" diff --git a/crates/federate/src/inboxes.rs b/crates/federate/src/inboxes.rs index 9869e5270..cda4da39b 100644 --- a/crates/federate/src/inboxes.rs +++ b/crates/federate/src/inboxes.rs @@ -8,9 +8,11 @@ use lemmy_db_schema::{ utils::{ActualDbPool, DbPool}, }; use lemmy_db_views_actor::structs::CommunityFollowerView; -use once_cell::sync::Lazy; use reqwest::Url; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + sync::LazyLock, +}; /// interval with which new additions to community_followers are queried. /// @@ -21,7 +23,7 @@ use std::collections::{HashMap, HashSet}; /// currently fairly high because of the current structure of storing inboxes for every person, not /// having a separate list of shared_inboxes, and the architecture of having every instance queue be /// fully separate. (see https://github.com/LemmyNet/lemmy/issues/3958) -static FOLLOW_ADDITIONS_RECHECK_DELAY: Lazy = Lazy::new(|| { +static FOLLOW_ADDITIONS_RECHECK_DELAY: LazyLock = LazyLock::new(|| { if *LEMMY_TEST_FAST_FEDERATION { chrono::TimeDelta::try_seconds(1).expect("TimeDelta out of bounds") } else { @@ -31,8 +33,8 @@ static FOLLOW_ADDITIONS_RECHECK_DELAY: Lazy = Lazy::new(|| { /// The same as FOLLOW_ADDITIONS_RECHECK_DELAY, but triggering when the last person on an instance /// unfollows a specific remote community. This is expected to happen pretty rarely and updating it /// in a timely manner is not too important. -static FOLLOW_REMOVALS_RECHECK_DELAY: Lazy = - Lazy::new(|| chrono::TimeDelta::try_hours(1).expect("TimeDelta out of bounds")); +static FOLLOW_REMOVALS_RECHECK_DELAY: LazyLock = + LazyLock::new(|| chrono::TimeDelta::try_hours(1).expect("TimeDelta out of bounds")); #[async_trait] pub trait DataSource: Send + Sync { diff --git a/crates/federate/src/util.rs b/crates/federate/src/util.rs index afbe957a5..e10a01c30 100644 --- a/crates/federate/src/util.rs +++ b/crates/federate/src/util.rs @@ -18,10 +18,15 @@ use lemmy_db_schema::{ utils::{get_conn, DbPool}, }; use moka::future::Cache; -use once_cell::sync::Lazy; use reqwest::Url; use serde_json::Value; -use std::{fmt::Debug, future::Future, pin::Pin, sync::Arc, time::Duration}; +use std::{ + fmt::Debug, + future::Future, + pin::Pin, + sync::{Arc, LazyLock}, + time::Duration, +}; use tokio::{task::JoinHandle, time::sleep}; use tokio_util::sync::CancellationToken; @@ -29,7 +34,7 @@ use tokio_util::sync::CancellationToken; /// Should only be used for federation tests since it significantly increases CPU and DB load of the /// federation queue. This is intentionally a separate flag from other flags like debug_assertions, /// since this is a invasive change we only need rarely. -pub(crate) static LEMMY_TEST_FAST_FEDERATION: Lazy = Lazy::new(|| { +pub(crate) static LEMMY_TEST_FAST_FEDERATION: LazyLock = LazyLock::new(|| { std::env::var("LEMMY_TEST_FAST_FEDERATION") .map(|s| !s.is_empty()) .unwrap_or(false) @@ -49,7 +54,7 @@ pub(crate) static LEMMY_TEST_FAST_FEDERATION: Lazy = Lazy::new(|| { /// If the delay is too short, the workers (one per federated instance) will wake up too /// often and consume a lot of CPU. If the delay is long, then activities on low-traffic instances /// will on average take delay/2 seconds to federate. -pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy = Lazy::new(|| { +pub(crate) static WORK_FINISHED_RECHECK_DELAY: LazyLock = LazyLock::new(|| { if *LEMMY_TEST_FAST_FEDERATION { Duration::from_millis(100) } else { @@ -61,7 +66,7 @@ pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy = Lazy::new(|| { /// /// This cache is common to all the instance workers and prevents there from being more than one /// call per N seconds between each DB query to find max(activity_id). -pub(crate) static CACHE_DURATION_LATEST_ID: Lazy = Lazy::new(|| { +pub(crate) static CACHE_DURATION_LATEST_ID: LazyLock = LazyLock::new(|| { if *LEMMY_TEST_FAST_FEDERATION { // in test mode, we use the same cache duration as the recheck delay so when recheck happens // data is fresh, accelerating the time the tests take. @@ -132,8 +137,8 @@ pub(crate) async fn get_actor_cached( actor_type: ActorType, actor_apub_id: &Url, ) -> Result> { - static CACHE: Lazy>> = - Lazy::new(|| Cache::builder().max_capacity(10000).build()); + static CACHE: LazyLock>> = + LazyLock::new(|| Cache::builder().max_capacity(10000).build()); CACHE .try_get_with(actor_apub_id.clone(), async { let url = actor_apub_id.clone().into(); @@ -172,8 +177,8 @@ pub(crate) async fn get_activity_cached( pool: &mut DbPool<'_>, activity_id: ActivityId, ) -> Result { - static ACTIVITIES: Lazy> = - Lazy::new(|| Cache::builder().max_capacity(10000).build()); + static ACTIVITIES: LazyLock> = + LazyLock::new(|| Cache::builder().max_capacity(10000).build()); ACTIVITIES .try_get_with(activity_id, async { let row = SentActivity::read(pool, activity_id) @@ -195,7 +200,7 @@ pub(crate) async fn get_activity_cached( /// return the most current activity id (with 1 second cache) pub(crate) async fn get_latest_activity_id(pool: &mut DbPool<'_>) -> Result { - static CACHE: Lazy> = Lazy::new(|| { + static CACHE: LazyLock> = LazyLock::new(|| { Cache::builder() .time_to_live(*CACHE_DURATION_LATEST_ID) .build() diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index 0d18e4f1f..a614ba42d 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -30,7 +30,6 @@ reqwest = { workspace = true, features = ["stream"] } reqwest-middleware = { workspace = true } serde = { workspace = true } url = { workspace = true } -once_cell = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } urlencoding = { workspace = true } diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index f08f28c4a..f7e7d4059 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -25,7 +25,6 @@ use lemmy_utils::{ error::{LemmyError, LemmyErrorType, LemmyResult}, utils::markdown::{markdown_to_html, sanitize_html}, }; -use once_cell::sync::Lazy; use rss::{ extension::{dublincore::DublinCoreExtension, ExtensionBuilder, ExtensionMap}, Channel, @@ -34,7 +33,7 @@ use rss::{ Item, }; use serde::Deserialize; -use std::{collections::BTreeMap, str::FromStr}; +use std::{collections::BTreeMap, str::FromStr, sync::LazyLock}; const RSS_FETCH_LIMIT: i64 = 20; @@ -80,7 +79,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { ); } -static RSS_NAMESPACE: Lazy> = Lazy::new(|| { +static RSS_NAMESPACE: LazyLock> = LazyLock::new(|| { let mut h = BTreeMap::new(); h.insert( "dc".to_string(), diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 96d7d317c..10ffb57de 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -41,12 +41,66 @@ pub fn config( .service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete))); } -#[derive(Deserialize)] +trait ProcessUrl { + /// If thumbnail or format is given, this uses the pictrs process endpoint. + /// Otherwise, it uses the normal pictrs url (IE image/original). + fn process_url(&self, image_url: &str, pictrs_url: &Url) -> String; +} + +#[derive(Deserialize, Clone)] struct PictrsGetParams { format: Option, thumbnail: Option, } +impl ProcessUrl for PictrsGetParams { + fn process_url(&self, src: &str, pictrs_url: &Url) -> String { + if self.format.is_none() && self.thumbnail.is_none() { + format!("{}image/original/{}", pictrs_url, src) + } else { + // Take file type from name, or jpg if nothing is given + let format = self + .clone() + .format + .unwrap_or_else(|| src.split('.').last().unwrap_or("jpg").to_string()); + + let mut url = format!("{}image/process.{}?src={}", pictrs_url, format, src); + + if let Some(size) = self.thumbnail { + url = format!("{url}&thumbnail={size}",); + } + url + } + } +} + +#[derive(Deserialize, Clone)] +pub struct ImageProxyParams { + url: String, + format: Option, + thumbnail: Option, +} + +impl ProcessUrl for ImageProxyParams { + fn process_url(&self, proxy_url: &str, pictrs_url: &Url) -> String { + if self.format.is_none() && self.thumbnail.is_none() { + format!("{}image/original?proxy={}", pictrs_url, proxy_url) + } else { + // Take file type from name, or jpg if nothing is given + let format = self + .clone() + .format + .unwrap_or_else(|| proxy_url.split('.').last().unwrap_or("jpg").to_string()); + + let mut url = format!("{}image/process.{}?proxy={}", pictrs_url, format, proxy_url); + + if let Some(size) = self.thumbnail { + url = format!("{url}&thumbnail={size}",); + } + url + } + } +} fn adapt_request( request: &HttpRequest, client: &ClientWithMiddleware, @@ -103,7 +157,13 @@ async fn upload( pictrs_alias: image.file.to_string(), pictrs_delete_token: image.delete_token.to_string(), }; - LocalImage::create(&mut context.pool(), &form).await?; + + let protocol_and_hostname = context.settings().get_protocol_and_hostname(); + let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?; + + // Also store the details for the image + let details_form = image.details.build_image_details_form(&thumbnail_url); + LocalImage::create(&mut context.pool(), &form, &details_form).await?; } } @@ -127,23 +187,10 @@ async fn full_res( // If there are no query params, the URL is original let pictrs_config = context.settings().pictrs_config()?; - let url = if params.format.is_none() && params.thumbnail.is_none() { - format!("{}image/original/{}", pictrs_config.url, name,) - } else { - // Take file type from name, or jpg if nothing is given - let format = params - .format - .unwrap_or_else(|| name.split('.').last().unwrap_or("jpg").to_string()); - let mut url = format!("{}image/process.{}?src={}", pictrs_config.url, format, name,); + let processed_url = params.process_url(name, &pictrs_config.url); - if let Some(size) = params.thumbnail { - url = format!("{url}&thumbnail={size}",); - } - url - }; - - image(url, req, &client).await + image(processed_url, req, &client).await } async fn image( @@ -202,11 +249,6 @@ async fn delete( Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream()))) } -#[derive(Deserialize)] -pub struct ImageProxyParams { - url: String, -} - pub async fn image_proxy( Query(params): Query, req: HttpRequest, @@ -220,9 +262,10 @@ pub async fn image_proxy( RemoteImage::validate(&mut context.pool(), url.clone().into()).await?; let pictrs_config = context.settings().pictrs_config()?; - let url = format!("{}image/original?proxy={}", pictrs_config.url, ¶ms.url); - image(url, req, &client).await + let processed_url = params.process_url(¶ms.url, &pictrs_config.url); + + image(processed_url, req, &client).await } fn make_send(mut stream: S) -> impl Stream + Send + Unpin + 'static diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 046c0a872..e94fce9d6 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -39,12 +39,10 @@ full = [ "dep:urlencoding", "dep:doku", "dep:url", - "dep:once_cell", "dep:smart-default", "dep:enum-map", "dep:futures", "dep:tokio", - "dep:openssl", "dep:html2text", "dep:lettre", "dep:uuid", @@ -59,13 +57,11 @@ tracing-error = { workspace = true, optional = true } itertools = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true, optional = true } -once_cell = { workspace = true, optional = true } url = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } reqwest-middleware = { workspace = true, optional = true } strum = { workspace = true } -strum_macros = { workspace = true } futures = { workspace = true, optional = true } diesel = { workspace = true, features = ["chrono"], optional = true } http = { workspace = true, optional = true } @@ -74,15 +70,16 @@ uuid = { workspace = true, features = ["serde", "v4"], optional = true } rosetta-i18n = { workspace = true, optional = true } tokio = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true } -openssl = { version = "0.10.64", 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", features = [ +lettre = { version = "0.11.7", default-features = false, features = [ + "builder", "tokio1", - "tokio1-native-tls", + "tokio1-rustls-tls", + "smtp-transport", ], optional = true } -markdown-it = { version = "0.6.0", optional = true } +markdown-it = { version = "0.6.1", optional = true } ts-rs = { workspace = true, optional = true } enum-map = { workspace = true, optional = true } cfg-if = "1" diff --git a/crates/utils/src/apub.rs b/crates/utils/src/apub.rs deleted file mode 100644 index 53e069d77..000000000 --- a/crates/utils/src/apub.rs +++ /dev/null @@ -1,26 +0,0 @@ -use openssl::{pkey::PKey, rsa::Rsa}; -use std::io::{Error, ErrorKind}; - -pub struct Keypair { - pub private_key: String, - pub public_key: String, -} - -/// Generate the asymmetric keypair for ActivityPub HTTP signatures. -pub fn generate_actor_keypair() -> Result { - let rsa = Rsa::generate(2048)?; - let pkey = PKey::from_rsa(rsa)?; - let public_key = pkey.public_key_to_pem()?; - let private_key = pkey.private_key_to_pem_pkcs8()?; - let key_to_string = |key| match String::from_utf8(key) { - Ok(s) => Ok(s), - Err(e) => Err(Error::new( - ErrorKind::Other, - format!("Failed converting key to string: {e}"), - )), - }; - Ok(Keypair { - private_key: key_to_string(private_key)?, - public_key: key_to_string(public_key)?, - }) -} diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 324c08ccb..860dad6fd 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -1,7 +1,7 @@ use cfg_if::cfg_if; use serde::{Deserialize, Serialize}; use std::fmt::Debug; -use strum_macros::{Display, EnumIter}; +use strum::{Display, EnumIter}; #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)] #[cfg_attr(feature = "full", derive(ts_rs::TS))] @@ -38,6 +38,8 @@ pub enum LemmyErrorType { NotTopAdmin, NotTopMod, NotLoggedIn, + NotHigherMod, + NotHigherAdmin, SiteBan, Deleted, BannedFromCommunity, @@ -177,6 +179,8 @@ pub enum LemmyErrorType { UrlWithoutDomain, InboxTimeout, Unknown(String), + CantDeleteSite, + UrlLengthOverflow, } cfg_if! { diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 1adb3f6cf..5a5e76d2a 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -2,7 +2,6 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "full")] { - pub mod apub; pub mod cache_header; pub mod email; pub mod rate_limit; diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index 5f1b6f7f5..a3c6f6a27 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -1,15 +1,15 @@ use enum_map::EnumMap; -use once_cell::sync::Lazy; use std::{ collections::HashMap, hash::Hash, net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::LazyLock, time::Instant, }; -use strum_macros::AsRefStr; +use strum::{AsRefStr, Display}; use tracing::debug; -static START_TIME: Lazy = Lazy::new(Instant::now); +static START_TIME: LazyLock = LazyLock::new(Instant::now); /// Smaller than `std::time::Instant` because it uses a smaller integer for seconds and doesn't /// store nanoseconds @@ -66,7 +66,7 @@ impl Bucket { } } -#[derive(Debug, enum_map::Enum, Copy, Clone, AsRefStr)] +#[derive(Debug, enum_map::Enum, Copy, Clone, Display, AsRefStr)] pub enum ActionType { Message, Register, diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index 6efa3fdd3..aba1a4fb1 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -1,9 +1,8 @@ use crate::{error::LemmyResult, location_info}; use anyhow::{anyhow, Context}; use deser_hjson::from_str; -use once_cell::sync::Lazy; use regex::Regex; -use std::{env, fs, io::Error}; +use std::{env, fs, io::Error, sync::LazyLock}; use urlencoding::encode; pub mod structs; @@ -12,7 +11,7 @@ use structs::{DatabaseConnection, PictrsConfig, PictrsImageMode, Settings}; static DEFAULT_CONFIG_FILE: &str = "config/config.hjson"; -pub static SETTINGS: Lazy = Lazy::new(|| { +pub static SETTINGS: LazyLock = LazyLock::new(|| { if env::var("LEMMY_INITIALIZE_WITH_DEFAULT_SETTINGS").is_ok() { println!( "LEMMY_INITIALIZE_WITH_DEFAULT_SETTINGS was set, any configuration file has been ignored." @@ -24,7 +23,7 @@ pub static SETTINGS: Lazy = Lazy::new(|| { } }); -static WEBFINGER_REGEX: Lazy = Lazy::new(|| { +static WEBFINGER_REGEX: LazyLock = LazyLock::new(|| { Regex::new(&format!( "^acct:([a-zA-Z0-9_]{{3,}})@{}$", SETTINGS.hostname diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs index 87e2cb3a2..7ed553e06 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -1,14 +1,14 @@ use crate::{error::LemmyResult, settings::SETTINGS, LemmyErrorType}; use markdown_it::{plugins::cmark::inline::image::Image, MarkdownIt}; -use once_cell::sync::Lazy; use regex::RegexSet; +use std::sync::LazyLock; use url::Url; use urlencoding::encode; mod link_rule; mod spoiler_rule; -static MARKDOWN_PARSER: Lazy = Lazy::new(|| { +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); diff --git a/crates/utils/src/utils/markdown/spoiler_rule.rs b/crates/utils/src/utils/markdown/spoiler_rule.rs index 3f12807fd..caced310a 100644 --- a/crates/utils/src/utils/markdown/spoiler_rule.rs +++ b/crates/utils/src/utils/markdown/spoiler_rule.rs @@ -34,8 +34,8 @@ use markdown_it::{ NodeValue, Renderer, }; -use once_cell::sync::Lazy; use regex::Regex; +use std::sync::LazyLock; #[derive(Debug)] struct SpoilerBlock { @@ -46,8 +46,8 @@ const SPOILER_PREFIX: &str = "::: spoiler "; const SPOILER_SUFFIX: &str = ":::"; const SPOILER_SUFFIX_NEWLINE: &str = ":::\n"; -static SPOILER_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^::: spoiler .*$").expect("compile spoiler markdown regex.")); +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. diff --git a/crates/utils/src/utils/mention.rs b/crates/utils/src/utils/mention.rs index 7e5e5f27c..c7cc2043f 100644 --- a/crates/utils/src/utils/mention.rs +++ b/crates/utils/src/utils/mention.rs @@ -1,8 +1,8 @@ use itertools::Itertools; -use once_cell::sync::Lazy; use regex::Regex; +use std::sync::LazyLock; -static MENTIONS_REGEX: Lazy = Lazy::new(|| { +static MENTIONS_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"@(?P[\w.]+)@(?P[a-zA-Z0-9._:-]+)").expect("compile regex") }); // TODO nothing is done with community / group webfingers yet, so just ignore those for now diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index 8891411a5..0a59e2fea 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -1,16 +1,16 @@ use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use itertools::Itertools; -use once_cell::sync::Lazy; use regex::{Regex, RegexBuilder, RegexSet}; +use std::sync::LazyLock; use url::{ParseError, Url}; // From here: https://github.com/vector-im/element-android/blob/develop/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt#L35 -static VALID_MATRIX_ID_REGEX: Lazy = Lazy::new(|| { +static VALID_MATRIX_ID_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^@[A-Za-z0-9\x21-\x39\x3B-\x7F]+:[A-Za-z0-9.-]+(:[0-9]{2,5})?$") .expect("compile regex") }); // taken from https://en.wikipedia.org/wiki/UTM_parameters -static CLEAN_URL_PARAMS_REGEX: Lazy = Lazy::new(|| { +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)=", ) @@ -21,6 +21,7 @@ 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 URL_MAX_LENGTH: usize = 2000; const ALT_TEXT_MAX_LENGTH: usize = 1500; const SITE_NAME_MAX_LENGTH: usize = 20; const SITE_NAME_MIN_LENGTH: usize = 1; @@ -87,12 +88,12 @@ fn has_newline(name: &str) -> bool { } pub fn is_valid_actor_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> { - static VALID_ACTOR_NAME_REGEX_EN: Lazy = - Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex")); - static VALID_ACTOR_NAME_REGEX_AR: Lazy = - Lazy::new(|| Regex::new(r"^[\p{Arabic}0-9_]{3,}$").expect("compile regex")); - static VALID_ACTOR_NAME_REGEX_RU: Lazy = - Lazy::new(|| Regex::new(r"^[\p{Cyrillic}0-9_]{3,}$").expect("compile regex")); + static VALID_ACTOR_NAME_REGEX_EN: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex")); + static VALID_ACTOR_NAME_REGEX_AR: LazyLock = + LazyLock::new(|| Regex::new(r"^[\p{Arabic}0-9_]{3,}$").expect("compile regex")); + static VALID_ACTOR_NAME_REGEX_RU: LazyLock = + LazyLock::new(|| Regex::new(r"^[\p{Cyrillic}0-9_]{3,}$").expect("compile regex")); let check = name.chars().count() <= actor_name_max_length && !has_newline(name); @@ -284,11 +285,17 @@ pub fn check_site_visibility_valid( } } -pub fn check_url_scheme(url: &Url) -> LemmyResult<()> { +pub fn is_valid_url(url: &Url) -> LemmyResult<()> { if !ALLOWED_POST_URL_SCHEMES.contains(&url.scheme()) { Err(LemmyErrorType::InvalidUrlScheme)? } + max_length_check( + url.as_str(), + URL_MAX_LENGTH, + LemmyErrorType::UrlLengthOverflow, + )?; + Ok(()) } @@ -349,7 +356,6 @@ mod tests { utils::validation::{ build_and_check_regex, check_site_visibility_valid, - check_url_scheme, check_urls_are_valid, clean_url_params, is_url_blocked, @@ -358,11 +364,13 @@ mod tests { is_valid_display_name, is_valid_matrix_id, is_valid_post_title, + is_valid_url, site_description_length_check, site_name_length_check, BIO_MAX_LENGTH, SITE_DESCRIPTION_MAX_LENGTH, SITE_NAME_MAX_LENGTH, + URL_MAX_LENGTH, }, }; use pretty_assertions::assert_eq; @@ -580,15 +588,27 @@ mod tests { } #[test] - fn test_check_url_scheme() -> LemmyResult<()> { - assert!(check_url_scheme(&Url::parse("http://example.com")?).is_ok()); - assert!(check_url_scheme(&Url::parse("https://example.com")?).is_ok()); - assert!(check_url_scheme(&Url::parse("https://example.com")?).is_ok()); - assert!(check_url_scheme(&Url::parse("ftp://example.com")?).is_err()); - assert!(check_url_scheme(&Url::parse("javascript:void")?).is_err()); + fn test_check_url_valid() -> LemmyResult<()> { + assert!(is_valid_url(&Url::parse("http://example.com")?).is_ok()); + assert!(is_valid_url(&Url::parse("https://example.com")?).is_ok()); + assert!(is_valid_url(&Url::parse("https://example.com")?).is_ok()); + assert!(is_valid_url(&Url::parse("ftp://example.com")?) + .is_err_and(|e| e.error_type.eq(&LemmyErrorType::InvalidUrlScheme))); + assert!(is_valid_url(&Url::parse("javascript:void")?) + .is_err_and(|e| e.error_type.eq(&LemmyErrorType::InvalidUrlScheme))); let magnet_link="magnet:?xt=urn:btih:4b390af3891e323778959d5abfff4b726510f14c&dn=Ravel%20Complete%20Piano%20Sheet%20Music%20-%20Public%20Domain&tr=udp%3A%2F%2Fopen.tracker.cl%3A1337%2Fannounce"; - assert!(check_url_scheme(&Url::parse(magnet_link)?).is_ok()); + assert!(is_valid_url(&Url::parse(magnet_link)?).is_ok()); + + // Also make sure the length overflow hits an error + let mut long_str = "http://example.com/test=".to_string(); + for _ in 1..URL_MAX_LENGTH { + long_str.push('X'); + } + let long_url = Url::parse(&long_str)?; + assert!( + is_valid_url(&long_url).is_err_and(|e| e.error_type.eq(&LemmyErrorType::UrlLengthOverflow)) + ); Ok(()) } diff --git a/crates/utils/translations b/crates/utils/translations index ee2cffac8..7adddded5 160000 --- a/crates/utils/translations +++ b/crates/utils/translations @@ -1 +1 @@ -Subproject commit ee2cffac809ad466644f061ad79ac577b6c2e4fd +Subproject commit 7adddded581fcd965ab33b91c5fe10e0d7247208 diff --git a/docker/Dockerfile b/docker/Dockerfile index 5125e3c3c..156d30dcc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,11 +1,11 @@ -# syntax=docker/dockerfile:1.8 -ARG RUST_VERSION=1.78 +# syntax=docker/dockerfile:1.9 +ARG RUST_VERSION=1.80 ARG CARGO_BUILD_FEATURES=default ARG RUST_RELEASE_MODE=debug ARG AMD_BUILDER_IMAGE=rust:${RUST_VERSION} # Repo: https://github.com/raskyld/lemmy-cross-toolchains -ARG ARM_BUILDER_IMAGE="ghcr.io/raskyld/aarch64-lemmy-linux-gnu:v0.3.0" +ARG ARM_BUILDER_IMAGE="ghcr.io/raskyld/aarch64-lemmy-linux-gnu:v0.4.0" ARG AMD_RUNNER_IMAGE=debian:bookworm-slim ARG ARM_RUNNER_IMAGE=debian:bookworm-slim diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 655182e1a..c1d8d359b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -97,7 +97,7 @@ services: logging: *default-logging postgres: - image: postgres:16-alpine + image: pgautoupgrade/pgautoupgrade:16-alpine # this needs to match the database host in lemmy.hson # Tune your settings via # https://pgtune.leopard.in.ua/#/ diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 72a8145ec..090ad71a8 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -20,7 +20,7 @@ x-lemmy-default: &lemmy-default restart: always x-postgres-default: &postgres-default - image: postgres:16-alpine + image: pgautoupgrade/pgautoupgrade:15-alpine environment: - POSTGRES_USER=lemmy - POSTGRES_PASSWORD=password diff --git a/migrations/2024-05-05-162540_add_image_detail_table/down.sql b/migrations/2024-05-05-162540_add_image_detail_table/down.sql new file mode 100644 index 000000000..819277c7f --- /dev/null +++ b/migrations/2024-05-05-162540_add_image_detail_table/down.sql @@ -0,0 +1,7 @@ +ALTER TABLE remote_image + ADD UNIQUE (link), + DROP CONSTRAINT remote_image_pkey, + ADD COLUMN id serial PRIMARY KEY; + +DROP TABLE image_details; + diff --git a/migrations/2024-05-05-162540_add_image_detail_table/up.sql b/migrations/2024-05-05-162540_add_image_detail_table/up.sql new file mode 100644 index 000000000..9b2ed9658 --- /dev/null +++ b/migrations/2024-05-05-162540_add_image_detail_table/up.sql @@ -0,0 +1,15 @@ +-- Drop the id column from the remote_image table, just use link +ALTER TABLE remote_image + DROP COLUMN id, + ADD PRIMARY KEY (link), + DROP CONSTRAINT remote_image_link_key; + +-- No good way to do references here unfortunately, unless we combine the images tables +-- The link should be the URL, not the pictrs_alias, to allow joining from post.thumbnail_url +CREATE TABLE image_details ( + link text PRIMARY KEY, + width integer NOT NULL, + height integer NOT NULL, + content_type text NOT NULL +); + diff --git a/migrations/2024-06-24-000000_ap_id_triggers/down.sql b/migrations/2024-06-24-000000_ap_id_triggers/down.sql new file mode 100644 index 000000000..72312eccf --- /dev/null +++ b/migrations/2024-06-24-000000_ap_id_triggers/down.sql @@ -0,0 +1,9 @@ +ALTER TABLE comment + ALTER COLUMN ap_id SET DEFAULT generate_unique_changeme (); + +ALTER TABLE post + ALTER COLUMN ap_id SET DEFAULT generate_unique_changeme (); + +ALTER TABLE private_message + ALTER COLUMN ap_id SET DEFAULT generate_unique_changeme (); + diff --git a/migrations/2024-06-24-000000_ap_id_triggers/up.sql b/migrations/2024-06-24-000000_ap_id_triggers/up.sql new file mode 100644 index 000000000..86e266d46 --- /dev/null +++ b/migrations/2024-06-24-000000_ap_id_triggers/up.sql @@ -0,0 +1,9 @@ +ALTER TABLE comment + ALTER COLUMN ap_id DROP DEFAULT; + +ALTER TABLE post + ALTER COLUMN ap_id DROP DEFAULT; + +ALTER TABLE private_message + ALTER COLUMN ap_id DROP DEFAULT; + diff --git a/migrations/2024-07-01-014711_exponential_controversy/down.sql b/migrations/2024-07-01-014711_exponential_controversy/down.sql new file mode 100644 index 000000000..15a7f296a --- /dev/null +++ b/migrations/2024-07-01-014711_exponential_controversy/down.sql @@ -0,0 +1,17 @@ +UPDATE + post_aggregates +SET + controversy_rank = CASE WHEN downvotes <= 0 + OR upvotes <= 0 THEN + 0 + ELSE + (upvotes + downvotes) * CASE WHEN upvotes > downvotes THEN + downvotes::float / upvotes::float + ELSE + upvotes::float / downvotes::float + END + END +WHERE + upvotes > 0 + AND downvotes > 0; + diff --git a/migrations/2024-07-01-014711_exponential_controversy/up.sql b/migrations/2024-07-01-014711_exponential_controversy/up.sql new file mode 100644 index 000000000..87a700de3 --- /dev/null +++ b/migrations/2024-07-01-014711_exponential_controversy/up.sql @@ -0,0 +1,17 @@ +UPDATE + post_aggregates +SET + controversy_rank = CASE WHEN downvotes <= 0 + OR upvotes <= 0 THEN + 0 + ELSE + (upvotes + downvotes) ^ CASE WHEN upvotes > downvotes THEN + downvotes::float / upvotes::float + ELSE + upvotes::float / downvotes::float + END + END +WHERE + upvotes > 0 + AND downvotes > 0; + diff --git a/migrations/2024-08-03-155932_increase_post_url_max_length/down.sql b/migrations/2024-08-03-155932_increase_post_url_max_length/down.sql new file mode 100644 index 000000000..d25918578 --- /dev/null +++ b/migrations/2024-08-03-155932_increase_post_url_max_length/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE post + ALTER COLUMN url TYPE varchar(512); + diff --git a/migrations/2024-08-03-155932_increase_post_url_max_length/up.sql b/migrations/2024-08-03-155932_increase_post_url_max_length/up.sql new file mode 100644 index 000000000..7c6818d22 --- /dev/null +++ b/migrations/2024-08-03-155932_increase_post_url_max_length/up.sql @@ -0,0 +1,5 @@ +-- Change the post url max limit to 2000 +-- From here: https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers#417184 +ALTER TABLE post + ALTER COLUMN url TYPE varchar(2000); + diff --git a/renovate.json b/renovate.json index 1911d651b..8d57f0aa8 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], - "schedule": ["before 4am on the first day of the month"] + "schedule": ["before 4am on the first day of the month"], + "automerge": true } diff --git a/scripts/db_perf.sh b/scripts/db_perf.sh index ef4b2751a..1d53e0d37 100755 --- a/scripts/db_perf.sh +++ b/scripts/db_perf.sh @@ -6,7 +6,7 @@ set -e CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -cd $CWD/../ +cd "$CWD/../" source scripts/start_dev_db.sh diff --git a/scripts/dump_schema.sh b/scripts/dump_schema.sh index f783be26b..c32cf20e5 100755 --- a/scripts/dump_schema.sh +++ b/scripts/dump_schema.sh @@ -5,7 +5,7 @@ set -e CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -cd $CWD/../ +cd "$CWD/../" source scripts/start_dev_db.sh diff --git a/scripts/lint.sh b/scripts/lint.sh index 7a93b5f54..c9f0f5dd7 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -3,7 +3,7 @@ set -e CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -cd $CWD/../ +cd "$CWD/../" # Format rust files cargo +nightly fmt @@ -12,6 +12,6 @@ cargo +nightly fmt taplo format # Format sql files -find migrations -type f -name '*.sql' -exec pg_format -i {} + +find migrations crates/db_schema/replaceable_schema -type f -name '*.sql' -exec pg_format -i {} + cargo clippy --workspace --fix --allow-staged --allow-dirty --tests --all-targets --all-features -- -D warnings diff --git a/scripts/postgres_12_to_15_upgrade.sh b/scripts/postgres_12_to_15_upgrade.sh index 0850e8fca..9af9af64e 100755 --- a/scripts/postgres_12_to_15_upgrade.sh +++ b/scripts/postgres_12_to_15_upgrade.sh @@ -1,48 +1,8 @@ #!/bin/sh set -e -echo "Do not stop in the middle of this upgrade, wait until you see the message: Upgrade complete." - -echo "Stopping lemmy and all services..." -sudo docker-compose stop - -echo "Make sure postgres is started..." -sudo docker-compose up -d postgres -sleep 20s - -echo "Exporting the Database to 12_15.dump.sql ..." -sudo docker-compose exec -T postgres pg_dumpall -c -U lemmy > 12_15_dump.sql -echo "Done." - -echo "Stopping postgres..." -sudo docker-compose stop postgres -sleep 20s - -echo "Removing the old postgres folder" -sudo rm -rf volumes/postgres - -echo "Updating docker-compose to use postgres version 15." -sed -i "s/image: postgres:.*/image: postgres:15-alpine/" ./docker-compose.yml - -echo "Starting up new postgres..." -sudo docker-compose up -d postgres -sleep 20s - -echo "Importing the database...." -cat 12_15_dump.sql | sudo docker-compose exec -T postgres psql -U lemmy -echo "Done." - -POSTGRES_PASSWORD=$(grep "POSTGRES_PASSWORD" ./docker-compose.yml | cut -d"=" -f2) - -echo "Fixing a weird password issue with postgres 15" -sudo docker-compose exec -T postgres psql -U lemmy -c "alter user lemmy with password '$POSTGRES_PASSWORD'" -sudo docker-compose restart postgres - -echo "Setting correct perms for pictrs folder" -sudo chown -R 991:991 volumes/pictrs +echo "Updating docker-compose to use postgres version 16." +sudo sed -i "s/image: .*postgres:.*/image: pgautoupgrade\/pgautoupgrade:16-alpine/" ./docker-compose.yml echo "Starting up lemmy..." sudo docker-compose up -d - -echo "A copy of your old database is at 12_15.dump.sql . You can delete this file if the upgrade went smoothly." -echo "Upgrade complete." diff --git a/scripts/postgres_15_to_16_upgrade.sh b/scripts/postgres_15_to_16_upgrade.sh index d83803a4f..9af9af64e 100755 --- a/scripts/postgres_15_to_16_upgrade.sh +++ b/scripts/postgres_15_to_16_upgrade.sh @@ -1,42 +1,8 @@ #!/bin/sh set -e -echo "Do not stop in the middle of this upgrade, wait until you see the message: Upgrade complete." - -echo "Stopping lemmy and all services..." -sudo docker compose stop - -echo "Make sure postgres is started..." -sudo docker compose up -d postgres -echo "Waiting..." -sleep 20s - -echo "Exporting the Database to 15_16.dump.sql ..." -sudo docker compose exec -T postgres pg_dumpall -c -U lemmy | sudo tee 15_16_dump.sql > /dev/null -echo "Done." - -echo "Stopping postgres..." -sudo docker compose stop postgres -echo "Waiting..." -sleep 20s - -echo "Removing the old postgres folder" -sudo rm -rf volumes/postgres - -echo "Updating docker compose to use postgres version 16." -sudo sed -i "s/image: .*postgres:.*/image: docker.io\/postgres:16-alpine/" ./docker-compose.yml - -echo "Starting up new postgres..." -sudo docker compose up -d postgres -echo "Waiting..." -sleep 20s - -echo "Importing the database...." -sudo cat 15_16_dump.sql | sudo docker compose exec -T postgres psql -U lemmy -echo "Done." +echo "Updating docker-compose to use postgres version 16." +sudo sed -i "s/image: .*postgres:.*/image: pgautoupgrade\/pgautoupgrade:16-alpine/" ./docker-compose.yml echo "Starting up lemmy..." -sudo docker compose up -d - -echo "A copy of your old database is at 15_16.dump.sql . You can delete this file if the upgrade went smoothly." -echo "Upgrade complete." +sudo docker-compose up -d diff --git a/scripts/release.sh b/scripts/release.sh index 1cf7b168a..79209f3b4 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -8,7 +8,7 @@ third_semver=$(echo $new_tag | cut -d "." -f 3) # Goto the upper route CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -cd $CWD/../ +cd "$CWD/../" # The docker installs should only update for non release-candidates # IE, when the third semver is a number, not '2-rc' diff --git a/scripts/sql_format_check.sh b/scripts/sql_format_check.sh index fabc3b3ed..3a2710b84 100755 --- a/scripts/sql_format_check.sh +++ b/scripts/sql_format_check.sh @@ -5,7 +5,7 @@ set -e CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -cd $CWD/../ +cd "$CWD/../" # Copy the files to a temp dir TMP_DIR=$(mktemp -d) diff --git a/scripts/test-with-coverage.sh b/scripts/test-with-coverage.sh index e4dfcddf8..7a4905b8e 100755 --- a/scripts/test-with-coverage.sh +++ b/scripts/test-with-coverage.sh @@ -3,7 +3,7 @@ set -e CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -cd $CWD/../ +cd "$CWD/../" PACKAGE="$1" echo "$PACKAGE" diff --git a/scripts/test.sh b/scripts/test.sh index 04cc94f9d..e08148db0 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,7 +3,7 @@ set -e CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -cd $CWD/../ +cd "$CWD/../" PACKAGE="$1" TEST="$2" diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 2a15b4ca6..7b4b34158 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -82,6 +82,7 @@ use lemmy_api::{ }, registration_applications::{ approve::approve_registration_application, + get::get_registration_application, list::list_registration_applications, unread_count::get_unread_registration_application_count, }, @@ -361,6 +362,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { "/registration_application/approve", web::put().to(approve_registration_application), ) + .route( + "/registration_application", + web::get().to(get_registration_application), + ) .route("/list_all_media", web::get().to(list_all_media)) .service( web::scope("/purge") diff --git a/src/code_migrations.rs b/src/code_migrations.rs index fd4ef66de..ae6155bb6 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -468,12 +468,11 @@ async fn initialize_local_site_2022_10_10( }; let person_inserted = Person::create(pool, &person_form).await?; - let local_user_form = LocalUserInsertForm::builder() - .person_id(person_inserted.id) - .password_encrypted(setup.admin_password.clone()) - .email(setup.admin_email.clone()) - .admin(Some(true)) - .build(); + let local_user_form = LocalUserInsertForm { + email: setup.admin_email.clone(), + admin: Some(true), + ..LocalUserInsertForm::new(person_inserted.id, setup.admin_password.clone()) + }; LocalUser::create(pool, &local_user_form, vec![]).await?; }; diff --git a/src/main.rs b/src/main.rs index dd17d6eb8..d7244d2d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,17 @@ use clap::Parser; use lemmy_server::{init_logging, start_lemmy_server, CmdArgs}; use lemmy_utils::{error::LemmyResult, settings::SETTINGS}; +pub extern crate rustls; + #[tokio::main] pub async fn main() -> LemmyResult<()> { init_logging(&SETTINGS.opentelemetry_url)?; let args = CmdArgs::parse(); + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rustls crypto provider"); + #[cfg(not(feature = "embed-pictrs"))] start_lemmy_server(args).await?; #[cfg(feature = "embed-pictrs")] diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 2885b5f74..ae6187a53 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -559,9 +559,9 @@ mod tests { #[tokio::test] #[serial] - async fn test_nodeinfo_voyager_lemmy_ml() -> LemmyResult<()> { + async fn test_nodeinfo_lemmy_ml() -> LemmyResult<()> { let client = ClientBuilder::new(client_builder(&Settings::default()).build()?).build(); - let form = build_update_instance_form("voyager.lemmy.ml", &client) + let form = build_update_instance_form("lemmy.ml", &client) .await .ok_or(LemmyErrorType::CouldntFindObject)?; assert_eq!( diff --git a/src/session_middleware.rs b/src/session_middleware.rs index 8b3090a47..a72d84920 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -146,10 +146,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("123456".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) .await