diff --git a/.rustfmt.toml b/.rustfmt.toml index 80c01a69b..dfeeb3bfd 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -3,3 +3,5 @@ edition = "2021" imports_layout = "HorizontalVertical" imports_granularity = "Crate" group_imports = "One" +wrap_comments = true +comment_width = 100 diff --git a/.woodpecker.yml b/.woodpecker.yml index 4c9af5441..f8f4eb5f3 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -3,6 +3,7 @@ variables: - &rust_image "rust:1.77" + - &rust_nightly_image "rustlang/rust:nightly" - &install_pnpm "corepack enable pnpm" - &slow_check_paths - event: pull_request @@ -24,15 +25,17 @@ variables: "diesel.toml", ".gitmodules", ] - -# Broken for cron jobs currently, see -# https://github.com/woodpecker-ci/woodpecker/issues/1716 -# clone: -# git: -# image: woodpeckerci/plugin-git -# settings: -# recursive: true -# submodule_update_remote: true + - 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: @@ -66,7 +69,7 @@ steps: - event: pull_request cargo_fmt: - image: rustlang/rust:nightly + image: *rust_nightly_image environment: # store cargo data in repo folder so that it gets cached between steps CARGO_HOME: .cargo_home @@ -77,11 +80,9 @@ steps: - event: pull_request cargo_machete: - image: rustlang/rust:nightly + image: *rust_nightly_image commands: - - 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_binstall - cargo binstall -y cargo-machete - cargo machete when: @@ -133,26 +134,17 @@ steps: when: *slow_check_paths check_diesel_schema: - image: willsquire/diesel-cli + image: *rust_image environment: CARGO_HOME: .cargo_home DATABASE_URL: postgres://lemmy:password@database:5432/lemmy commands: + - <<: *install_diesel_cli - diesel migration run - diesel print-schema --config-file=diesel.toml > tmp.schema - diff tmp.schema crates/db_schema/src/schema.rs when: *slow_check_paths - check_diesel_migration_revertable: - image: willsquire/diesel-cli - environment: - CARGO_HOME: .cargo_home - DATABASE_URL: postgres://lemmy:password@database:5432/lemmy - commands: - - diesel migration run - - diesel migration redo - when: *slow_check_paths - check_db_perf_tool: image: *rust_image environment: @@ -194,6 +186,44 @@ steps: - cargo test --workspace --no-fail-fast when: *slow_check_paths + check_diesel_migration: + # TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server + image: *rust_image + environment: + LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy + RUST_BACKTRACE: "1" + CARGO_HOME: .cargo_home + DATABASE_URL: postgres://lemmy:password@database:5432/lemmy + PGUSER: lemmy + PGPASSWORD: password + PGHOST: database + PGDATABASE: lemmy + commands: + # Install diesel_cli + - <<: *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 + - diesel migration redo + # Run schema setup twice, which fails on the 2nd time if `DROP SCHEMA IF EXISTS r CASCADE` drops the wrong things + - alias lemmy_schema_setup="target/lemmy_server --disable-scheduled-tasks --disable-http-server --disable-activity-sending" + - lemmy_schema_setup + - lemmy_schema_setup + # Make sure that the newest migration is revertable with the `r` schema + - diesel migration redo + # Check for changes in the schema, which would be caused by an incorrect migration + - psql -c "DROP SCHEMA IF EXISTS r CASCADE;" + - pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f after.sqldump + - diff before.sqldump after.sqldump + when: *slow_check_paths + run_federation_tests: image: node:20-bookworm-slim environment: @@ -248,10 +278,11 @@ steps: publish_to_crates_io: image: *rust_image commands: - - cargo install cargo-workspaces + - <<: *install_binstall + # Install cargo-workspaces + - cargo binstall -y cargo-workspaces - cp -r migrations crates/db_schema/ - - cargo login "$CARGO_API_TOKEN" - - cargo workspaces publish --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}" + - cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}" secrets: [cargo_api_token] when: - event: tag diff --git a/Cargo.lock b/Cargo.lock index 979e9af10..b358fc82d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,14 +16,14 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772" [[package]] name = "activitypub_federation" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e16130d5914e6483f99bde5a9bb97ca62e1f359e0b9791c8ebd5c7abd50fe8e" +checksum = "ac8ff2d0151ce9ac02eb29e4a58b41d28693f141f7963d4bfabd2f9d402ecec7" dependencies = [ "activitystreams-kinds", "actix-web", "async-trait", - "base64 0.21.7", + "base64 0.22.1", "bytes", "chrono", "derive_builder", @@ -31,7 +31,7 @@ dependencies = [ "enum_delegate", "futures", "futures-core", - "http", + "http 0.2.12", "http-signature-normalization", "http-signature-normalization-reqwest", "httpdate", @@ -41,8 +41,8 @@ dependencies = [ "openssl", "pin-project-lite", "regex", - "reqwest", - "reqwest-middleware", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", "serde", "serde_json", "sha2", @@ -68,7 +68,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "bytes", "futures-core", "futures-sink", @@ -81,9 +81,9 @@ dependencies = [ [[package]] name = "actix-cors" -version = "0.6.5" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" +checksum = "f9e772b3bcafe335042b5db010ab7c09013dad6eac4915c91d8d50902769f331" dependencies = [ "actix-utils", "actix-web", @@ -96,9 +96,9 @@ dependencies = [ [[package]] name = "actix-form-data" -version = "0.7.0-beta.6" +version = "0.7.0-beta.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0588d156cb871d8c237d55ce398e2a65727370fb98352ba5b65c15a2f834b0f" +checksum = "6c2355a841a5d9a6c616d6a4f31336064116d206e6c1830de22730f983613a05" dependencies = [ "actix-multipart", "actix-web", @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743" +checksum = "4eb9843d84c775696c37d9a418bbb01b932629d01870722c0f13eb3f95e2536d" dependencies = [ "actix-codec", "actix-rt", @@ -122,8 +122,8 @@ dependencies = [ "actix-tls", "actix-utils", "ahash", - "base64 0.21.7", - "bitflags 2.4.2", + "base64 0.22.1", + "bitflags 2.5.0", "brotli", "bytes", "bytestring", @@ -131,8 +131,8 @@ dependencies = [ "encoding_rs", "flate2", "futures-core", - "h2", - "http", + "h2 0.3.26", + "http 0.2.12", "httparse", "httpdate", "itoa", @@ -157,7 +157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -185,13 +185,15 @@ dependencies = [ [[package]] name = "actix-router" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", - "http", + "cfg-if", + "http 0.2.12", "regex", + "regex-lite", "serde", "tracing", ] @@ -236,9 +238,9 @@ dependencies = [ [[package]] name = "actix-tls" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cce60a2f2b477bc72e5cde0af1812a6e82d8fd85b5570a5dcf2a5bf2c5be5f" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" dependencies = [ "actix-rt", "actix-service", @@ -248,11 +250,9 @@ dependencies = [ "pin-project-lite", "rustls-pki-types", "tokio", - "tokio-rustls 0.23.4", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tokio-util", "tracing", - "webpki-roots 0.22.6", ] [[package]] @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.5.1" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984" +checksum = "b1cf67dadb19d7c95e5a299e2dda24193b89d5d4f33a3b9800888ede9e19aa32" dependencies = [ "actix-codec", "actix-http", @@ -296,7 +296,7 @@ dependencies = [ "mime", "once_cell", "pin-project-lite", - "regex", + "regex-lite", "serde", "serde_json", "serde_urlencoded", @@ -315,7 +315,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -335,15 +335,17 @@ dependencies = [ [[package]] name = "actix-web-prom" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23f332a652836b8f3a6876103c70c9ed436d0e69fa779ab5d7f57b1d5c8d488" +checksum = "76743e67d4e7efa9fc2ac7123de0dd7b2ca592668e19334f1d81a3b077afc6ac" dependencies = [ "actix-web", "futures-core", + "log", "pin-project-lite", "prometheus", "regex", + "strfmt", ] [[package]] @@ -376,9 +378,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -400,9 +402,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -421,47 +423,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -469,9 +472,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" dependencies = [ "backtrace", ] @@ -494,9 +497,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.6" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" dependencies = [ "flate2", "futures-core", @@ -507,11 +510,13 @@ dependencies = [ [[package]] name = "async-lock" -version = "2.8.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ - "event-listener", + "event-listener 4.0.3", + "event-listener-strategy", + "pin-project-lite", ] [[package]] @@ -533,31 +538,31 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "async-trait" -version = "0.1.78" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "atom_syndication" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571832dcff775e26562e8e6930cd483de5587301d40d3a3b85d532b6383e15a7" +checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30" dependencies = [ "chrono", "derive_builder", "diligent-date-parser", "never", - "quick-xml 0.30.0", + "quick-xml 0.31.0", ] [[package]] @@ -567,10 +572,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] -name = "autocfg" -version = "1.1.0" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "aws-lc-rs" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8487b59d62764df8231cb371c459314df895b41756df457a1fb1243d65c89195" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15eb61145320320eb919d9bab524617a7aa4216c78d342fae3a758bc33073e4" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] [[package]] name = "axum" @@ -583,11 +622,11 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -609,8 +648,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -619,9 +658,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -658,9 +697,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -677,7 +716,7 @@ dependencies = [ "async-trait", "futures-channel", "futures-util", - "parking_lot 0.12.1", + "parking_lot 0.12.2", "tokio", ] @@ -697,7 +736,7 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "blowfish", "getrandom", "subtle", @@ -713,6 +752,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.5.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.65", + "which", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -736,9 +798,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] @@ -770,9 +832,9 @@ checksum = "bfb5ea45aeb912f2dd334834e64ecf674a6673d57c73e9d12de0298b9bf98ee8" [[package]] name = "brotli" -version = "3.5.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -781,9 +843,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -791,21 +853,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" - -[[package]] -name = "bytecount" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" [[package]] name = "byteorder" @@ -815,9 +871,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytestring" @@ -828,15 +884,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "camino" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" -dependencies = [ - "serde", -] - [[package]] name = "captcha" version = "0.0.9" @@ -851,36 +898,15 @@ dependencies = [ "serde_json", ] -[[package]] -name = "cargo-platform" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", -] - [[package]] name = "cc" -version = "1.0.90" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -889,6 +915,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -897,9 +932,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -907,7 +942,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -916,7 +951,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "stacker", ] @@ -931,10 +966,21 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.3" +name = "clang-sys" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -949,19 +995,19 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim 0.11.1", ] [[package]] name = "clap_derive" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -979,6 +1025,15 @@ dependencies = [ "chrono", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -1014,20 +1069,29 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.14.0" @@ -1040,7 +1104,7 @@ dependencies = [ "ron", "serde", "serde_json", - "toml 0.8.12", + "toml 0.8.13", "yaml-rust", ] @@ -1063,8 +1127,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" dependencies = [ "futures-core", - "prost 0.12.3", - "prost-types 0.12.3", + "prost 0.12.6", + "prost-types 0.12.6", "tonic 0.10.2", "tracing-core", ] @@ -1105,7 +1169,7 @@ dependencies = [ "futures-task", "hdrhistogram", "humantime", - "prost-types 0.12.3", + "prost-types 0.12.6", "serde", "serde_json", "thread_local", @@ -1193,18 +1257,18 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -1220,9 +1284,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -1246,22 +1310,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.14.4" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" -dependencies = [ - "darling_core 0.20.8", - "darling_macro 0.20.8", + "darling_core 0.20.9", + "darling_macro 0.20.9", ] [[package]] @@ -1280,30 +1334,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.14.4" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 2.0.53", + "strsim 0.11.1", + "syn 2.0.65", ] [[package]] @@ -1319,24 +1359,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.14.4" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ - "darling_core 0.14.4", + "darling_core 0.20.9", "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" -dependencies = [ - "darling_core 0.20.8", - "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -1346,10 +1375,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.9", + "parking_lot_core 0.9.10", ] [[package]] @@ -1367,11 +1396,10 @@ dependencies = [ [[package]] name = "deadpool" -version = "0.10.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" dependencies = [ - "async-trait", "deadpool-runtime", "num_cpus", "tokio", @@ -1379,23 +1407,37 @@ dependencies = [ [[package]] name = "deadpool-runtime" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" dependencies = [ "tokio", ] [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", "zeroize", ] +[[package]] +name = "der_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1419,33 +1461,33 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" dependencies = [ - "darling 0.14.4", + "darling 0.20.9", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.65", ] [[package]] name = "derive_builder_macro" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 1.0.109", + "syn 2.0.65", ] [[package]] @@ -1472,11 +1514,11 @@ dependencies = [ [[package]] name = "diesel" -version = "2.1.5" +version = "2.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559" +checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "chrono", "diesel_derives", @@ -1512,30 +1554,30 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "diesel-derive-newtype" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ed4d69628c8de8eb4c3f50cddb0678cba3c5b4cbe3cb1067d4d6c62ca47e4e" +checksum = "d5adf688c584fe33726ce0e2898f608a2a92578ac94a4a92fcecf73214fe0716" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "diesel_derives" -version = "2.1.3" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d02eecb814ae714ffe61ddc2db2dd03e6c49a42e269b5001355500d431cce0c" +checksum = "14701062d6bed917b5c7103bdffaee1e4609279e240488ad24e7bd979ca6866c" dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -1565,7 +1607,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -1620,9 +1662,15 @@ dependencies = [ [[package]] name = "downcast-rs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "dyn-clone" @@ -1632,9 +1680,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "elementtree" @@ -1647,11 +1695,11 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "memchr", ] @@ -1661,75 +1709,11 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" -[[package]] -name = "encoding" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" -dependencies = [ - "encoding-index-japanese", - "encoding-index-korean", - "encoding-index-simpchinese", - "encoding-index-singlebyte", - "encoding-index-tradchinese", -] - -[[package]] -name = "encoding-index-japanese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-korean" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-simpchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-singlebyte" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-tradchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding_index_tests" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" - [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1757,7 +1741,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -1792,28 +1776,45 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] -name = "error-chain" -version = "0.12.4" +name = "event-listener" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ - "version_check", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] name = "event-listener" -version = "2.5.3" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] [[package]] name = "eyre" @@ -1852,9 +1853,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fdeflate" @@ -1872,27 +1873,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" [[package]] -name = "flate2" -version = "1.0.28" +name = "flagset" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1" + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", ] -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "spin 0.9.8", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1933,6 +1928,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futf" version = "0.1.5" @@ -1999,7 +2000,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -2053,9 +2054,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -2078,17 +2079,36 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap 2.2.5", + "http 0.2.12", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -2112,9 +2132,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -2167,14 +2187,23 @@ dependencies = [ ] [[package]] -name = "hostname" -version = "0.3.1" +name = "home" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 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", - "match_cfg", - "winapi", + "windows", ] [[package]] @@ -2198,25 +2227,25 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be92446e11d68f5d71367d571c229d09ced1f24ab6d08ea0bff329d5f6c0b2a3" dependencies = [ - "html5ever", + "html5ever 0.26.0", "jni", "lazy_static", - "markup5ever_rcdom", + "markup5ever_rcdom 0.2.0", "percent-encoding", "regex", ] [[package]] name = "html2text" -version = "0.6.0" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74cda84f06c1cc83476f79ae8e2e892b626bdadafcb227baec54c918cadc18a0" +checksum = "8c66ee488a63a92237d5b48875b7e05bb293be8fb2894641c8118b60c08ab5ef" dependencies = [ - "html5ever", - "markup5ever", + "html5ever 0.27.0", + "markup5ever 0.12.1", "tendril", + "thiserror", "unicode-width", - "xml5ever", ] [[package]] @@ -2227,12 +2256,26 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.11.0", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "http" version = "0.2.12" @@ -2244,6 +2287,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -2251,7 +2305,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -2274,8 +2351,8 @@ dependencies = [ "base64 0.13.1", "http-signature-normalization", "httpdate", - "reqwest", - "reqwest-middleware", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", "sha2", "thiserror", "tokio", @@ -2309,9 +2386,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -2324,17 +2401,41 @@ dependencies = [ ] [[package]] -name = "hyper-rustls" -version = "0.24.2" +name = "hyper" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http", - "hyper", - "rustls 0.21.10", + "http 1.1.0", + "hyper 1.3.1", + "hyper-util", + "rustls 0.22.4", + "rustls-pki-types", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.25.0", + "tower-service", ] [[package]] @@ -2343,7 +2444,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.28", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2356,12 +2457,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "i-love-jesus" version = "0.1.0" @@ -2379,7 +2500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8215279f83f9b829403812f845aa2d0dd5966332aa2fd0334a375256f3dd0322" dependencies = [ "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -2469,12 +2590,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -2489,30 +2610,25 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -2522,15 +2638,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -2541,10 +2648,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.10" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jni" @@ -2568,9 +2684,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -2586,13 +2702,14 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "8.3.0" +version = "9.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" dependencies = [ "base64 0.21.7", - "pem 1.1.1", - "ring 0.16.20", + "js-sys", + "pem", + "ring", "serde", "serde_json", "simple_asn1", @@ -2610,15 +2727,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lemmy_api" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "activitypub_federation", "actix-web", "actix-web-httpauth", "anyhow", - "base64 0.21.7", + "base64 0.22.1", "bcrypt", "captcha", "chrono", @@ -2641,13 +2764,13 @@ dependencies = [ [[package]] name = "lemmy_api_common" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "activitypub_federation", "actix-web", "anyhow", "chrono", - "encoding", + "encoding_rs", "enum-map", "futures", "getrandom", @@ -2662,8 +2785,8 @@ dependencies = [ "once_cell", "pretty_assertions", "regex", - "reqwest", - "reqwest-middleware", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", "rosetta-i18n", "serde", "serde_with", @@ -2679,7 +2802,7 @@ dependencies = [ [[package]] name = "lemmy_api_crud" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "accept-language", "activitypub_federation", @@ -2702,7 +2825,7 @@ dependencies = [ [[package]] name = "lemmy_apub" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "activitypub_federation", "actix-web", @@ -2715,8 +2838,8 @@ dependencies = [ "futures", "html2md", "html2text", - "http", - "itertools 0.12.1", + "http 0.2.12", + "itertools 0.13.0", "lemmy_api_common", "lemmy_db_schema", "lemmy_db_views", @@ -2725,7 +2848,7 @@ dependencies = [ "moka", "once_cell", "pretty_assertions", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "serde_with", @@ -2740,7 +2863,7 @@ dependencies = [ [[package]] name = "lemmy_db_perf" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "anyhow", "clap", @@ -2755,14 +2878,14 @@ dependencies = [ [[package]] name = "lemmy_db_schema" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "activitypub_federation", "anyhow", "async-trait", "bcrypt", "chrono", - "deadpool 0.10.0", + "deadpool 0.12.1", "diesel", "diesel-async", "diesel-derive-enum", @@ -2776,7 +2899,7 @@ dependencies = [ "once_cell", "pretty_assertions", "regex", - "rustls 0.21.10", + "rustls 0.23.8", "serde", "serde_json", "serde_with", @@ -2785,7 +2908,7 @@ dependencies = [ "strum_macros", "tokio", "tokio-postgres", - "tokio-postgres-rustls 0.10.0", + "tokio-postgres-rustls", "tracing", "ts-rs", "typed-builder", @@ -2795,7 +2918,7 @@ dependencies = [ [[package]] name = "lemmy_db_views" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "actix-web", "chrono", @@ -2817,7 +2940,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_actor" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "chrono", "diesel", @@ -2837,7 +2960,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_moderator" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "diesel", "diesel-async", @@ -2849,7 +2972,7 @@ dependencies = [ [[package]] name = "lemmy_federate" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "activitypub_federation", "anyhow", @@ -2861,10 +2984,12 @@ dependencies = [ "lemmy_apub", "lemmy_db_schema", "lemmy_db_views_actor", + "lemmy_utils", "moka", "once_cell", - "reqwest", + "reqwest 0.11.27", "serde_json", + "serial_test", "tokio", "tokio-util", "tracing", @@ -2872,7 +2997,7 @@ dependencies = [ [[package]] name = "lemmy_routes" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "activitypub_federation", "actix-web", @@ -2885,8 +3010,8 @@ dependencies = [ "lemmy_db_views_actor", "lemmy_utils", "once_cell", - "reqwest", - "reqwest-middleware", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", "rss", "serde", "tokio", @@ -2897,7 +3022,7 @@ dependencies = [ [[package]] name = "lemmy_server" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "activitypub_federation", "actix-cors", @@ -2923,9 +3048,9 @@ dependencies = [ "pict-rs", "pretty_assertions", "prometheus", - "reqwest", - "reqwest-middleware", - "reqwest-tracing", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", + "reqwest-tracing 0.4.8", "serde_json", "serial_test", "tokio", @@ -2940,7 +3065,7 @@ dependencies = [ [[package]] name = "lemmy_utils" -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" dependencies = [ "actix-web", "anyhow", @@ -2951,16 +3076,16 @@ dependencies = [ "enum-map", "futures", "html2text", - "http", - "itertools 0.12.1", + "http 0.2.12", + "itertools 0.13.0", "lettre", "markdown-it", "once_cell", "openssl", "pretty_assertions", "regex", - "reqwest", - "reqwest-middleware", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", "rosetta-build", "rosetta-i18n", "serde", @@ -2979,12 +3104,12 @@ dependencies = [ [[package]] name = "lettre" -version = "0.11.4" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357ff5edb6d8326473a64c82cf41ddf78ab116f89668c50c4fac1b321e5e80f4" +checksum = "1a62049a808f1c4e2356a2a380bd5f2aca3b011b0b482cf3b914ba1731426969" dependencies = [ "async-trait", - "base64 0.21.7", + "base64 0.22.1", "chumsky", "email-encoding", "email_address", @@ -3007,18 +3132,25 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] [[package]] name = "line-wrap" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" -dependencies = [ - "safemem", -] +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" [[package]] name = "linked-hash-map" @@ -3037,15 +3169,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - -[[package]] -name = "linux-raw-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-channel" @@ -3066,9 +3192,9 @@ checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -3130,7 +3256,21 @@ checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ "log", "phf 0.10.1", - "phf_codegen", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.2", + "phf_codegen 0.11.2", "string_cache", "string_cache_codegen", "tendril", @@ -3142,17 +3282,23 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" dependencies = [ - "html5ever", - "markup5ever", + "html5ever 0.26.0", + "markup5ever 0.11.0", "tendril", - "xml5ever", + "xml5ever 0.17.0", ] [[package]] -name = "match_cfg" -version = "0.1.0" +name = "markup5ever_rcdom" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever 0.27.0", + "markup5ever 0.12.1", + "tendril", + "xml5ever 0.18.0", +] [[package]] name = "matchers" @@ -3169,6 +3315,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "540f1c43aed89909c0cc0cc604e3bb2f7e7a341a3728a9e6cfe760e733cd11ed" + [[package]] name = "md-5" version = "0.10.6" @@ -3192,9 +3344,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "metrics" @@ -3208,19 +3360,22 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf4e7146e30ad172c42c39b3246864bd2d3c6396780711a1baf749cfe423e21" +checksum = "5d58e362dc7206e9456ddbcdbd53c71ba441020e62104703075a69151e38d85f" dependencies = [ - "base64 0.21.7", - "hyper", - "indexmap 2.2.5", + "base64 0.22.1", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "indexmap 2.2.6", "ipnet", "metrics", "metrics-util", "quanta", "thiserror", "tokio", + "tracing", ] [[package]] @@ -3231,7 +3386,7 @@ checksum = "8b07a5eb561b8cbc16be2d216faf7757f9baf3bfb94dbb0fae3df8387a5bb47f" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "metrics", "num_cpus", "quanta", @@ -3283,9 +3438,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", "simd-adler32", @@ -3304,22 +3459,28 @@ dependencies = [ ] [[package]] -name = "moka" -version = "0.12.5" +name = "mirai-annotations" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1911e88d5831f748a4097a43862d129e3c6fca831eecac9b8db6d01d93c9de2" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "moka" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0d88686dc561d743b40de8269b26eaf0dc58781bde087b0984646602021d08" dependencies = [ "async-lock", "async-trait", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", + "event-listener 5.3.0", "futures-util", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.12.2", "quanta", "rustc_version", - "skeptic", "smallvec", "tagptr", "thiserror", @@ -3338,9 +3499,6 @@ name = "nanorand" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom", -] [[package]] name = "native-tls" @@ -3394,11 +3552,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -3420,9 +3577,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -3458,7 +3615,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -3475,7 +3632,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -3486,9 +3643,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -3547,7 +3704,7 @@ dependencies = [ "async-trait", "futures", "futures-util", - "http", + "http 0.2.12", "opentelemetry 0.19.0", "opentelemetry-proto 0.2.0", "prost 0.11.9", @@ -3564,12 +3721,12 @@ checksum = "1a016b8d9495c639af2145ac22387dcb88e44118e45320d9238fbf4e7889abcb" dependencies = [ "async-trait", "futures-core", - "http", + "http 0.2.12", "opentelemetry 0.22.0", "opentelemetry-proto 0.5.0", "opentelemetry-semantic-conventions", "opentelemetry_sdk 0.22.1", - "prost 0.12.3", + "prost 0.12.6", "thiserror", "tokio", "tonic 0.11.0", @@ -3596,7 +3753,7 @@ checksum = "3a8fddc9b68f5b80dae9d6f510b88e02396f006ad48cac349411fbecc80caae4" dependencies = [ "opentelemetry 0.22.0", "opentelemetry_sdk 0.22.1", - "prost 0.12.3", + "prost 0.12.6", "tonic 0.11.0", ] @@ -3687,6 +3844,12 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.11.2" @@ -3700,12 +3863,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", - "parking_lot_core 0.9.9", + "parking_lot_core 0.9.10", ] [[package]] @@ -3724,22 +3887,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" @@ -3749,21 +3912,21 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pem" -version = "1.1.1" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.13.1", + "base64 0.22.1", + "serde", ] [[package]] -name = "pem" -version = "3.0.3" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "base64 0.21.7", - "serde", + "base64ct", ] [[package]] @@ -3796,10 +3959,20 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" dependencies = [ - "phf_generator", + "phf_generator 0.10.0", "phf_shared 0.10.0", ] +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + [[package]] name = "phf_generator" version = "0.10.0" @@ -3810,6 +3983,16 @@ dependencies = [ "rand", ] +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand", +] + [[package]] name = "phf_shared" version = "0.10.0" @@ -3830,15 +4013,15 @@ dependencies = [ [[package]] name = "pict-rs" -version = "0.5.9" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e709cefc614ddbe5685b30298790a2fe8637523190c182c0215c18053ce7cfdf" +checksum = "27cb4fd629bb8a115b8ae2701e2c355f6e5e9ad6bf22d9d0e7b7b645fb0f81dc" dependencies = [ "actix-form-data", "actix-web", "async-trait", "barrel", - "base64 0.21.7", + "base64 0.22.1", "bb8", "blurhash-update", "clap", @@ -3849,7 +4032,6 @@ dependencies = [ "diesel", "diesel-async", "diesel-derive-enum", - "flume", "futures-core", "hex", "md-5", @@ -3861,12 +4043,12 @@ dependencies = [ "opentelemetry_sdk 0.22.1", "pin-project-lite", "refinery", - "reqwest", - "reqwest-middleware", - "reqwest-tracing", - "rustls 0.22.2", + "reqwest 0.12.4", + "reqwest-middleware 0.3.1", + "reqwest-tracing 0.5.0", + "rustls 0.23.8", "rustls-channel-resolver", - "rustls-pemfile 2.1.1", + "rustls-pemfile 2.1.2", "rusty-s3", "serde", "serde-tuple-vec-map", @@ -3880,9 +4062,9 @@ dependencies = [ "time", "tokio", "tokio-postgres", - "tokio-postgres-rustls 0.11.1", + "tokio-postgres-generic-rustls", "tokio-util", - "toml 0.8.12", + "toml 0.8.13", "tracing", "tracing-actix-web", "tracing-error", @@ -3891,7 +4073,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", - "webpki-roots 0.26.1", + "webpki-roots", ] [[package]] @@ -3911,14 +4093,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -3934,12 +4116,12 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plist" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" dependencies = [ "base64 0.21.7", - "indexmap 2.2.5", + "indexmap 2.2.6", "line-wrap", "quick-xml 0.31.0", "serde", @@ -4050,39 +4232,59 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.79" +name = "prettyplease" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.65", +] + +[[package]] +name = "proc-macro2" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] [[package]] name = "procfs" -version = "0.14.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" +checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 1.3.2", - "byteorder", + "bitflags 2.5.0", "hex", "lazy_static", - "rustix 0.36.17", + "procfs-core", + "rustix", +] + +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.5.0", + "hex", ] [[package]] name = "prometheus" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" dependencies = [ "cfg-if", "fnv", "lazy_static", "libc", "memchr", - "parking_lot 0.12.1", + "parking_lot 0.12.2", "procfs", "protobuf", "thiserror", @@ -4100,12 +4302,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive 0.12.3", + "prost-derive 0.12.6", ] [[package]] @@ -4123,15 +4325,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -4145,11 +4347,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "prost 0.12.3", + "prost 0.12.6", ] [[package]] @@ -4167,22 +4369,11 @@ dependencies = [ "cc", ] -[[package]] -name = "pulldown-cmark" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" -dependencies = [ - "bitflags 2.4.2", - "memchr", - "unicase", -] - [[package]] name = "quanta" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca0b7bac0b97248c40bb77288fc52029cf1459c0461ea1b05ee32ccf011de2c" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" dependencies = [ "crossbeam-utils", "libc", @@ -4199,7 +4390,6 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ - "encoding_rs", "memchr", "serde", ] @@ -4210,14 +4400,15 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ + "encoding_rs", "memchr", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -4260,11 +4451,11 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.0.1" +version = "11.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d86a7c4638d42c44551f4791a20e687dbb4c3de1f33c43dd71e355cd429def1" +checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", ] [[package]] @@ -4275,7 +4466,7 @@ checksum = "a25d631e41bfb5fdcde1d4e2215f62f7f0afa3ff11e26563765bd6ea1d229aeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -4297,10 +4488,19 @@ dependencies = [ ] [[package]] -name = "refinery" -version = "0.8.12" +name = "redox_syscall" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2783724569d96af53464d0711dff635cab7a4934df5e22e9fbc9e181523b83e" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "refinery" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0904191f0566c3d3e0091d5cc8dec22e663d77def2d247b16e7a438b188bf75d" dependencies = [ "refinery-core", "refinery-macros", @@ -4308,9 +4508,9 @@ dependencies = [ [[package]] name = "refinery-core" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d6c80329c0455510a8d42fce286ecb4b6bcd8c57e1816d9f2d6bd7379c2cc8" +checksum = "9bf253999e1899ae476c910b994959e341d84c4389ba9533d3dacbe06df04825" dependencies = [ "async-trait", "cfg-if", @@ -4323,34 +4523,35 @@ dependencies = [ "time", "tokio", "tokio-postgres", - "toml 0.8.12", + "toml 0.8.13", "url", "walkdir", ] [[package]] name = "refinery-macros" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab6e31e166a49d55cb09b62639e5ab9ba2e73f2f124336b06f6c321dc602779" +checksum = "bd81f69687fe8a1fa10995108b3ffc7cdbd63e682a4f8fbfd1020130780d7e17" dependencies = [ + "heck 0.4.1", "proc-macro2", "quote", "refinery-core", "regex", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -4370,9 +4571,15 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] +[[package]] +name = "regex-lite" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -4381,15 +4588,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.26" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "async-compression", "base64 0.21.7", @@ -4397,11 +4604,10 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-tls", "ipnet", "js-sys", @@ -4412,7 +4618,6 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -4421,7 +4626,6 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", @@ -4429,8 +4633,50 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.25.4", - "winreg", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.22.4", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.25.0", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg 0.52.0", ] [[package]] @@ -4441,13 +4687,28 @@ checksum = "5a735987236a8e238bf0296c7e351b999c188ccc11477f311b82b55c93984216" dependencies = [ "anyhow", "async-trait", - "http", - "reqwest", + "http 0.2.12", + "reqwest 0.11.27", "serde", "task-local-extensions", "thiserror", ] +[[package]] +name = "reqwest-middleware" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45d100244a467870f6cb763c4484d010a6bed6bd610b3676e3825d93fb4cfbd" +dependencies = [ + "anyhow", + "async-trait", + "http 1.1.0", + "reqwest 0.12.4", + "serde", + "thiserror", + "tower-service", +] + [[package]] name = "reqwest-tracing" version = "0.4.8" @@ -4457,15 +4718,31 @@ dependencies = [ "anyhow", "async-trait", "getrandom", - "matchit", + "matchit 0.7.3", "opentelemetry 0.16.0", - "reqwest", - "reqwest-middleware", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", "task-local-extensions", "tracing", "tracing-opentelemetry 0.16.0", ] +[[package]] +name = "reqwest-tracing" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b253954a1979e02eabccd7e9c3d61d8f86576108baa160775e7f160bb4e800a3" +dependencies = [ + "anyhow", + "async-trait", + "getrandom", + "http 1.1.0", + "matchit 0.8.2", + "reqwest 0.12.4", + "reqwest-middleware 0.3.1", + "tracing", +] + [[package]] name = "retain_mut" version = "0.1.9" @@ -4487,21 +4764,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9b1a3d5f46d53f4a3478e2be4a5a5ce5108ea58b100dcd139830eae7f79a3a1" -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -4512,7 +4774,7 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -4524,7 +4786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.5.0", "serde", "serde_derive", ] @@ -4551,21 +4813,27 @@ checksum = "2f8c01b9158de3aa5a7ac041a41c0e854d7adc3e473e7d7e2143eb5432bc5ba2" [[package]] name = "rss" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a" +checksum = "2f374fd66bb795938b78c021db1662d43a8ffbc42ec1ac25429fc4833b732751" dependencies = [ "atom_syndication", "derive_builder", "never", - "quick-xml 0.30.0", + "quick-xml 0.31.0", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" @@ -4578,77 +4846,55 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.17" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.1.4", - "windows-sys 0.45.0", -] - -[[package]] -name = "rustix" -version = "0.38.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" -dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.20.9" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", - "ring 0.16.20", - "sct", - "webpki", -] - -[[package]] -name = "rustls" -version = "0.21.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" -dependencies = [ - "log", - "ring 0.17.8", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" -dependencies = [ - "log", - "ring 0.17.8", + "ring", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79adb16721f56eb2d843e67676896a61ce7a0fa622dc18d3e372477a029d2740" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-channel-resolver" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbd1941204442f051576a9a7ea8e8db074ad7fd43db1eb3378c3633f9f9e166" +checksum = "fede2a247359da6b4998f7723ec6468c2d6a577a5d8c17e54f21806426ad2290" dependencies = [ "nanorand", - "rustls 0.22.2", + "rustls 0.23.8", ] [[package]] @@ -4662,46 +4908,37 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.3.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" -dependencies = [ - "ring 0.17.8", + "aws-lc-rs", + "ring", "rustls-pki-types", "untrusted 0.9.0", ] [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rusty-s3" @@ -4724,15 +4961,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" - -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -4743,6 +4974,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -4769,22 +5009,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "sct" -version = "0.7.1" +name = "sdd" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -4793,9 +5029,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -4808,24 +5044,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f9da09dc3f4dfdb6374cbffff7a2cffcec316874d4429899eefdc97b3b94dcd" dependencies = [ "bit-set", - "html5ever", - "markup5ever_rcdom", + "html5ever 0.26.0", + "markup5ever_rcdom 0.2.0", ] [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" -dependencies = [ - "serde", -] +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] @@ -4841,22 +5074,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -4873,9 +5106,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -4894,15 +5127,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_derive", "serde_json", @@ -4912,39 +5145,39 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ - "darling 0.20.8", + "darling 0.20.9", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "serial_test" -version = "2.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" dependencies = [ - "dashmap", "futures", - "lazy_static", "log", - "parking_lot 0.12.1", + "once_cell", + "parking_lot 0.12.2", + "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "2.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -4979,10 +5212,16 @@ dependencies = [ ] [[package]] -name = "signal-hook-registry" -version = "1.4.1" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -5036,21 +5275,6 @@ dependencies = [ "xml-builder", ] -[[package]] -name = "skeptic" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" -dependencies = [ - "bytecount", - "cargo_metadata", - "error-chain", - "glob", - "pulldown-cmark", - "tempfile", - "walkdir", -] - [[package]] name = "sketches-ddsketch" version = "0.2.2" @@ -5084,9 +5308,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smart-default" @@ -5096,33 +5320,24 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] [[package]] name = "spki" @@ -5157,6 +5372,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "strfmt" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65" + [[package]] name = "string_cache" version = "0.8.7" @@ -5165,7 +5386,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.12.2", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -5177,7 +5398,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ - "phf_generator", + "phf_generator 0.10.0", "phf_shared 0.10.0", "proc-macro2", "quote", @@ -5208,27 +5429,27 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -5250,9 +5471,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.53" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ "proc-macro2", "quote", @@ -5278,7 +5499,7 @@ dependencies = [ "fnv", "once_cell", "plist", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", "serde", "serde_derive", "serde_json", @@ -5331,7 +5552,7 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "rustix 0.38.31", + "rustix", "windows-sys 0.52.0", ] @@ -5357,22 +5578,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -5387,9 +5608,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -5408,9 +5629,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -5437,6 +5658,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "tokio" version = "1.37.0" @@ -5448,7 +5690,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot 0.12.2", "pin-project-lite", "signal-hook-registry", "socket2", @@ -5475,7 +5717,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -5501,7 +5743,7 @@ dependencies = [ "futures-channel", "futures-util", "log", - "parking_lot 0.12.1", + "parking_lot 0.12.2", "percent-encoding", "phf 0.11.2", "pin-project-lite", @@ -5515,62 +5757,51 @@ dependencies = [ ] [[package]] -name = "tokio-postgres-rustls" -version = "0.10.0" +name = "tokio-postgres-generic-rustls" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5831152cb0d3f79ef5523b357319ba154795d64c7078b2daa95a803b54057f" +checksum = "c8e98c31c29b2666fb28720739e11476166be4ead1610a37dcd7414bb124413a" dependencies = [ - "futures", - "ring 0.16.20", - "rustls 0.21.10", + "aws-lc-rs", + "rustls 0.23.8", "tokio", "tokio-postgres", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.0", + "x509-cert", ] [[package]] name = "tokio-postgres-rustls" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea13f22eda7127c827983bdaf0d7fff9df21c8817bab02815ac277a21143677" +checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" dependencies = [ - "futures", - "ring 0.17.8", - "rustls 0.22.2", + "ring", + "rustls 0.23.8", "tokio", "tokio-postgres", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "x509-certificate", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.10", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.2", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.8", "rustls-pki-types", "tokio", ] @@ -5588,16 +5819,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -5614,21 +5844,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.8", + "toml_edit 0.22.13", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -5639,7 +5869,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -5648,15 +5878,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.8" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.5", + "winnow 0.6.8", ] [[package]] @@ -5672,10 +5902,10 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", @@ -5703,10 +5933,10 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", @@ -5730,14 +5960,14 @@ dependencies = [ "axum", "base64 0.21.7", "bytes", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.12.3", + "prost 0.12.6", "tokio", "tokio-stream", "tower", @@ -5757,14 +5987,14 @@ dependencies = [ "axum", "base64 0.21.7", "bytes", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.12.3", + "prost 0.12.6", "tokio", "tokio-stream", "tower", @@ -5856,7 +6086,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -6019,28 +6249,28 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", "termcolor", ] [[package]] name = "typed-builder" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444d8748011b93cb168770e8092458cb0f8854f931ff82fdf6ddfbd72a9c933e" +checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352" +checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -6087,9 +6317,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -6226,7 +6456,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", "wasm-bindgen-shared", ] @@ -6260,7 +6490,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6286,9 +6516,9 @@ dependencies = [ [[package]] name = "wav" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65e199c799848b4f997072aa4d673c034f80f40191f97fe2f0a23f410be1609" +checksum = "99d97402f69875b579ec37f2aa52d1f455a1d6224251edba32e8c18a5da2698d" dependencies = [ "riff", ] @@ -6321,7 +6551,7 @@ checksum = "8d07b90492f7b6fe35f5298fcd01c663d3c453e8c302dc86c7292c6681b8117d" dependencies = [ "anyhow", "nom", - "reqwest", + "reqwest 0.11.27", "select", "serde", "thiserror", @@ -6330,41 +6560,17 @@ dependencies = [ [[package]] name = "webpage" -version = "1.6.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8598785beeb5af95abe95e7bb20c7e747d1188347080d6811d5a56d2b9a5f368" +checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" dependencies = [ - "html5ever", - "markup5ever_rcdom", + "html5ever 0.27.0", + "markup5ever_rcdom 0.3.0", "serde", "serde_json", + "url", ] -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[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.1" @@ -6374,6 +6580,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "whoami" version = "1.5.1" @@ -6403,11 +6621,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -6416,22 +6634,23 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -6449,22 +6668,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -6484,25 +6688,20 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6511,15 +6710,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -6529,15 +6722,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -6547,15 +6734,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -6565,15 +6752,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -6583,15 +6764,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -6601,15 +6776,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -6619,9 +6788,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -6634,9 +6803,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] @@ -6651,6 +6820,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", + "tls_codec", +] + [[package]] name = "x509-certificate" version = "0.23.1" @@ -6662,8 +6853,8 @@ dependencies = [ "chrono", "der", "hex", - "pem 3.0.3", - "ring 0.17.8", + "pem", + "ring", "signature", "spki", "thiserror", @@ -6684,7 +6875,18 @@ checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.11.0", +] + +[[package]] +name = "xml5ever" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c376f76ed09df711203e20c3ef5ce556f0166fa03d39590016c0fd625437fad" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", ] [[package]] @@ -6704,22 +6906,22 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] @@ -6739,32 +6941,32 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.65", ] [[package]] name = "zstd" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.0.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 6737d3b9c..d03d195cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.19.4-beta.3" +version = "0.19.4-rc.3" edition = "2021" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -67,8 +67,8 @@ members = [ [workspace.lints.clippy] cast_lossless = "deny" -complexity = "deny" -correctness = "deny" +complexity = { level = "deny", priority = -1 } +correctness = { level = "deny", priority = -1 } dbg_macro = "deny" explicit_into_iter_loop = "deny" explicit_iter_loop = "deny" @@ -79,37 +79,37 @@ inefficient_to_string = "deny" items-after-statements = "deny" manual_string_new = "deny" needless_collect = "deny" -perf = "deny" +perf = { level = "deny", priority = -1 } redundant_closure_for_method_calls = "deny" -style = "deny" -suspicious = "deny" +style = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } uninlined_format_args = "allow" unused_self = "deny" unwrap_used = "deny" [workspace.dependencies] -lemmy_api = { version = "=0.19.4-beta.3", path = "./crates/api" } -lemmy_api_crud = { version = "=0.19.4-beta.3", path = "./crates/api_crud" } -lemmy_apub = { version = "=0.19.4-beta.3", path = "./crates/apub" } -lemmy_utils = { version = "=0.19.4-beta.3", path = "./crates/utils", default-features = false } -lemmy_db_schema = { version = "=0.19.4-beta.3", path = "./crates/db_schema" } -lemmy_api_common = { version = "=0.19.4-beta.3", path = "./crates/api_common" } -lemmy_routes = { version = "=0.19.4-beta.3", path = "./crates/routes" } -lemmy_db_views = { version = "=0.19.4-beta.3", path = "./crates/db_views" } -lemmy_db_views_actor = { version = "=0.19.4-beta.3", path = "./crates/db_views_actor" } -lemmy_db_views_moderator = { version = "=0.19.4-beta.3", path = "./crates/db_views_moderator" } -lemmy_federate = { version = "=0.19.4-beta.3", path = "./crates/federate" } -activitypub_federation = { version = "0.5.4", default-features = false, features = [ +lemmy_api = { version = "=0.19.4-rc.3", path = "./crates/api" } +lemmy_api_crud = { version = "=0.19.4-rc.3", path = "./crates/api_crud" } +lemmy_apub = { version = "=0.19.4-rc.3", path = "./crates/apub" } +lemmy_utils = { version = "=0.19.4-rc.3", path = "./crates/utils", default-features = false } +lemmy_db_schema = { version = "=0.19.4-rc.3", path = "./crates/db_schema" } +lemmy_api_common = { version = "=0.19.4-rc.3", path = "./crates/api_common" } +lemmy_routes = { version = "=0.19.4-rc.3", path = "./crates/routes" } +lemmy_db_views = { version = "=0.19.4-rc.3", path = "./crates/db_views" } +lemmy_db_views_actor = { version = "=0.19.4-rc.3", path = "./crates/db_views_actor" } +lemmy_db_views_moderator = { version = "=0.19.4-rc.3", path = "./crates/db_views_moderator" } +lemmy_federate = { version = "=0.19.4-rc.3", path = "./crates/federate" } +activitypub_federation = { version = "0.5.6", default-features = false, features = [ "actix-web", ] } -diesel = "2.1.4" +diesel = "2.1.6" diesel_migrations = "2.1.0" diesel-async = "0.4.1" -serde = { version = "1.0.197", features = ["derive"] } -serde_with = "3.7.0" -actix-web = { version = "4.5.1", default-features = false, features = [ +serde = { version = "1.0.202", features = ["derive"] } +serde_with = "3.8.1" +actix-web = { version = "4.6.0", default-features = false, features = [ "macros", - "rustls", + "rustls-0_23", "compress-brotli", "compress-gzip", "compress-zstd", @@ -121,32 +121,32 @@ 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.26", features = ["json", "blocking", "gzip"] } -reqwest-middleware = "0.2.4" -reqwest-tracing = "0.4.7" +reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] } +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.0" -chrono = { version = "0.4.35", features = ["serde"], default-features = false } -serde_json = { version = "1.0.114", features = ["preserve_order"] } -base64 = "0.21.7" -uuid = { version = "1.7.0", features = ["serde", "v4"] } -async-trait = "0.1.77" +bcrypt = "0.15.1" +chrono = { version = "0.4.38", features = ["serde"], default-features = false } +serde_json = { version = "1.0.117", features = ["preserve_order"] } +base64 = "0.22.1" +uuid = { version = "1.8.0", features = ["serde", "v4"] } +async-trait = "0.1.80" captcha = "0.0.9" -anyhow = { version = "1.0.81", features = [ +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.1" -serial_test = "2.0.0" +typed-builder = "0.18.2" +serial_test = "3.1.1" tokio = { version = "1.37.0", features = ["full"] } -regex = "1.10.3" +regex = "1.10.4" once_cell = "1.19.0" -diesel-derive-newtype = "2.1.0" +diesel-derive-newtype = "2.1.2" diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } -strum = "0.25.0" -strum_macros = "0.25.3" -itertools = "0.12.1" +strum = "0.26.2" +strum_macros = "0.26.2" +itertools = "0.13.0" futures = "0.3.30" http = "0.2.12" rosetta-i18n = "0.1.3" @@ -157,15 +157,15 @@ ts-rs = { version = "7.1.1", features = [ "chrono-impl", "no-serde-warnings", ] } -rustls = { version = "0.21.10", features = ["dangerous_configuration"] } +rustls = { version = "0.23.8", features = ["ring"] } futures-util = "0.3.30" tokio-postgres = "0.7.10" -tokio-postgres-rustls = "0.10.0" +tokio-postgres-rustls = "0.12.0" urlencoding = "2.1.3" enum-map = "2.7" -moka = { version = "0.12.5", features = ["future"] } +moka = { version = "0.12.7", features = ["future"] } i-love-jesus = { version = "0.1.0" } -clap = { version = "4.5.2", features = ["derive"] } +clap = { version = "4.5.4", features = ["derive", "env"] } pretty_assertions = "1.4.0" [dependencies] @@ -196,15 +196,15 @@ tracing-opentelemetry = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true } console-subscriber = { version = "0.1.10", optional = true } opentelemetry-otlp = { version = "0.12.0", optional = true } -pict-rs = { version = "0.5.9", optional = true } +pict-rs = { version = "0.5.14", optional = true } tokio.workspace = true -actix-cors = "0.6.5" +actix-cors = "0.7.0" futures-util = { workspace = true } chrono = { workspace = true } -prometheus = { version = "0.13.3", features = ["process"] } +prometheus = { version = "0.13.4", features = ["process"] } serial_test = { workspace = true } clap = { workspace = true } -actix-web-prom = "0.7.0" +actix-web-prom = "0.8.0" [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/api_tests/.npmrc b/api_tests/.npmrc new file mode 100644 index 000000000..e941d13c2 --- /dev/null +++ b/api_tests/.npmrc @@ -0,0 +1 @@ +package-manager-strict=false diff --git a/api_tests/package.json b/api_tests/package.json index 60adac43c..b194dae30 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -6,10 +6,11 @@ "repository": "https://github.com/LemmyNet/lemmy", "author": "Dessalines", "license": "AGPL-3.0", + "packageManager": "pnpm@9.1.1+sha256.9551e803dcb7a1839fdf5416153a844060c7bce013218ce823410532504ac10b", "scripts": { "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'", "fix": "prettier --write src && eslint --fix src", - "api-test": "jest -i follow.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts && jest -i private_message.spec.ts && jest -i user.spec.ts && jest -i community.spec.ts && jest -i image.spec.ts", + "api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ", "api-test-follow": "jest -i follow.spec.ts", "api-test-comment": "jest -i comment.spec.ts", "api-test-post": "jest -i post.spec.ts", @@ -27,7 +28,7 @@ "eslint": "^8.57.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.19.4-alpha.16", + "lemmy-js-client": "0.19.4-alpha.18", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.4.4" diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 81508d740..42eb612e5 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -1,78 +1,1623 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -devDependencies: - '@types/jest': - specifier: ^29.5.12 - version: 29.5.12 - '@types/node': - specifier: ^20.12.4 - version: 20.12.4 - '@typescript-eslint/eslint-plugin': - specifier: ^7.5.0 - version: 7.5.0(@typescript-eslint/parser@7.5.0)(eslint@8.57.0)(typescript@5.4.4) - '@typescript-eslint/parser': - specifier: ^7.5.0 - version: 7.5.0(eslint@8.57.0)(typescript@5.4.4) - download-file-sync: - specifier: ^1.0.4 - version: 1.0.4 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - eslint-plugin-prettier: - specifier: ^5.1.3 - version: 5.1.3(eslint@8.57.0)(prettier@3.2.5) - jest: - specifier: ^29.5.0 - version: 29.7.0(@types/node@20.12.4) - lemmy-js-client: - specifier: 0.19.4-alpha.16 - version: 0.19.4-alpha.16 - prettier: - specifier: ^3.2.5 - version: 3.2.5 - ts-jest: - specifier: ^29.1.0 - version: 29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.4.4) - typescript: - specifier: ^5.4.4 - version: 5.4.4 +importers: + + .: + devDependencies: + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + '@types/node': + specifier: ^20.12.4 + version: 20.12.4 + '@typescript-eslint/eslint-plugin': + specifier: ^7.5.0 + version: 7.5.0(@typescript-eslint/parser@7.5.0)(eslint@8.57.0)(typescript@5.4.4) + '@typescript-eslint/parser': + specifier: ^7.5.0 + version: 7.5.0(eslint@8.57.0)(typescript@5.4.4) + download-file-sync: + specifier: ^1.0.4 + version: 1.0.4 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-plugin-prettier: + specifier: ^5.1.3 + version: 5.1.3(eslint@8.57.0)(prettier@3.2.5) + jest: + specifier: ^29.5.0 + version: 29.7.0(@types/node@20.12.4) + lemmy-js-client: + specifier: 0.19.4-alpha.18 + version: 0.19.4-alpha.18 + prettier: + specifier: ^3.2.5 + version: 3.2.5 + ts-jest: + specifier: ^29.1.0 + version: 29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.4.4) + typescript: + specifier: ^5.4.4 + version: 5.4.4 packages: - /@aashutoshrathi/word-wrap@1.2.6: + '@aashutoshrathi/word-wrap@1.2.6': resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} - dev: true - /@ampproject/remapping@2.2.1: + '@ampproject/remapping@2.2.1': resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.23.5': + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.23.5': + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.23.9': + resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.23.6': + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.23.6': + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-environment-visitor@7.22.20': + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-function-name@7.23.0': + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-hoist-variables@7.22.5': + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.22.15': + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.23.3': + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.22.5': + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-simple-access@7.22.5': + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-split-export-declaration@7.22.6': + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.23.4': + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.22.20': + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.23.5': + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.23.9': + resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.23.4': + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.23.9': + resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.23.3': + resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.23.3': + resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.23.9': + resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.23.9': + resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.23.9': + resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.10.0': + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.2': + resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.3': + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.1': + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.1.2': + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@jridgewell/trace-mapping@0.3.22': + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.5': + resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.12': + resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@20.12.4': + resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==} + + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.32': + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + + '@typescript-eslint/eslint-plugin@7.5.0': + resolution: {integrity: sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.5.0': + resolution: {integrity: sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.5.0': + resolution: {integrity: sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.5.0': + resolution: {integrity: sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.5.0': + resolution: {integrity: sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.5.0': + resolution: {integrity: sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.5.0': + resolution: {integrity: sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.5.0': + resolution: {integrity: sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + 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==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.0.1: + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + + browserslist@4.22.3: + resolution: {integrity: sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001581: + resolution: {integrity: sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.3: + 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 + + dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + download-file-sync@1.0.4: + resolution: {integrity: sha512-vH92qNH508jZZA12HQNq/aiMDfagr4JvjFiI17Bi8oYjsxwv5ZVIi7iHkYmUXxOQUr90tcVX+8EPePjAqG1Y0w==} + + electron-to-chromium@1.4.648: + resolution: {integrity: sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-prettier@5.1.3: + resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.1: + resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + lemmy-js-client@0.19.4-alpha.18: + resolution: {integrity: sha512-CUKRIiINZF2zOfK5WzBDF071LjMmRBFHwiSYBMGJyQP1zu8sPKCb/ptg25WWrf79Y4uOaVLctgHg3oEUXmSUmQ==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.0.4: + resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.8.8: + resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-jest@29.1.2: + resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} + engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + typescript@5.4.4: + resolution: {integrity: sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + update-browserslist-db@1.0.13: + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@aashutoshrathi/word-wrap@1.2.6': {} + + '@ampproject/remapping@2.2.1': dependencies: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.22 - dev: true - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} - engines: {node: '>=6.9.0'} + '@babel/code-frame@7.23.5': dependencies: '@babel/highlight': 7.23.4 chalk: 2.4.2 - dev: true - /@babel/compat-data@7.23.5: - resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/compat-data@7.23.5': {} - /@babel/core@7.23.9: - resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==} - engines: {node: '>=6.9.0'} + '@babel/core@7.23.9': dependencies: '@ampproject/remapping': 2.2.1 '@babel/code-frame': 7.23.5 @@ -91,61 +1636,38 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color - dev: true - /@babel/generator@7.23.6: - resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} - engines: {node: '>=6.9.0'} + '@babel/generator@7.23.6': dependencies: '@babel/types': 7.23.9 '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.22 jsesc: 2.5.2 - dev: true - /@babel/helper-compilation-targets@7.23.6: - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} - engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.23.6': dependencies: '@babel/compat-data': 7.23.5 '@babel/helper-validator-option': 7.23.5 browserslist: 4.22.3 lru-cache: 5.1.1 semver: 6.3.1 - dev: true - /@babel/helper-environment-visitor@7.22.20: - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/helper-environment-visitor@7.22.20': {} - /@babel/helper-function-name@7.23.0: - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} + '@babel/helper-function-name@7.23.0': dependencies: '@babel/template': 7.23.9 '@babel/types': 7.23.9 - dev: true - /@babel/helper-hoist-variables@7.22.5: - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} - engines: {node: '>=6.9.0'} + '@babel/helper-hoist-variables@7.22.5': dependencies: '@babel/types': 7.23.9 - dev: true - /@babel/helper-module-imports@7.22.15: - resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} - engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.22.15': dependencies: '@babel/types': 7.23.9 - dev: true - /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-environment-visitor': 7.22.20 @@ -153,211 +1675,118 @@ packages: '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 - dev: true - /@babel/helper-plugin-utils@7.22.5: - resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/helper-plugin-utils@7.22.5': {} - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} - engines: {node: '>=6.9.0'} + '@babel/helper-simple-access@7.22.5': dependencies: '@babel/types': 7.23.9 - dev: true - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} - engines: {node: '>=6.9.0'} + '@babel/helper-split-export-declaration@7.22.6': dependencies: '@babel/types': 7.23.9 - dev: true - /@babel/helper-string-parser@7.23.4: - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/helper-string-parser@7.23.4': {} - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/helper-validator-identifier@7.22.20': {} - /@babel/helper-validator-option@7.23.5: - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/helper-validator-option@7.23.5': {} - /@babel/helpers@7.23.9: - resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} - engines: {node: '>=6.9.0'} + '@babel/helpers@7.23.9': dependencies: '@babel/template': 7.23.9 '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 transitivePeerDependencies: - supports-color - dev: true - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} - engines: {node: '>=6.9.0'} + '@babel/highlight@7.23.4': dependencies: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true - /@babel/parser@7.23.9: - resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} - engines: {node: '>=6.0.0'} - hasBin: true + '@babel/parser@7.23.9': dependencies: '@babel/types': 7.23.9 - dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.9): - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.9): - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.9): - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.9): - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.9): - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.9): - resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.9): - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.9): - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.9): - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.9): - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.9): - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.9): - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.9): - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.9): - resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/template@7.23.9: - resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==} - engines: {node: '>=6.9.0'} + '@babel/template@7.23.9': dependencies: '@babel/code-frame': 7.23.5 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - dev: true - /@babel/traverse@7.23.9: - resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} - engines: {node: '>=6.9.0'} + '@babel/traverse@7.23.9': dependencies: '@babel/code-frame': 7.23.5 '@babel/generator': 7.23.6 @@ -371,39 +1800,23 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: true - /@babel/types@7.23.9: - resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} - engines: {node: '>=6.9.0'} + '@babel/types@7.23.9': dependencies: '@babel/helper-string-parser': 7.23.4 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - dev: true - /@bcoe/v8-coverage@0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true + '@bcoe/v8-coverage@0.2.3': {} - /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 eslint-visitor-keys: 3.4.3 - dev: true - /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true + '@eslint-community/regexpp@4.10.0': {} - /@eslint/eslintrc@2.1.4: - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 debug: 4.3.4 @@ -416,52 +1829,32 @@ packages: strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - dev: true - /@eslint/js@8.57.0: - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + '@eslint/js@8.57.0': {} - /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.2 debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - dev: true - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true + '@humanwhocodes/module-importer@1.0.1': {} - /@humanwhocodes/object-schema@2.0.2: - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} - dev: true + '@humanwhocodes/object-schema@2.0.2': {} - /@istanbuljs/load-nyc-config@1.1.0: - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 js-yaml: 3.14.1 resolve-from: 5.0.0 - dev: true - /@istanbuljs/schema@0.1.3: - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - dev: true + '@istanbuljs/schema@0.1.3': {} - /@jest/console@29.7.0: - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 '@types/node': 20.12.4 @@ -469,16 +1862,8 @@ packages: jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - dev: true - /@jest/core@29.7.0: - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + '@jest/core@29.7.0': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -512,38 +1897,26 @@ packages: - babel-plugin-macros - supports-color - ts-node - dev: true - /@jest/environment@29.7.0: - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/node': 20.12.4 jest-mock: 29.7.0 - dev: true - /@jest/expect-utils@29.7.0: - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect-utils@29.7.0': dependencies: jest-get-type: 29.6.3 - dev: true - /@jest/expect@29.7.0: - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect@29.7.0': dependencies: expect: 29.7.0 jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color - dev: true - /@jest/fake-timers@29.7.0: - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/fake-timers@29.7.0': dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 @@ -551,11 +1924,8 @@ packages: jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 - dev: true - /@jest/globals@29.7.0: - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/globals@29.7.0': dependencies: '@jest/environment': 29.7.0 '@jest/expect': 29.7.0 @@ -563,16 +1933,8 @@ packages: jest-mock: 29.7.0 transitivePeerDependencies: - supports-color - dev: true - /@jest/reporters@29.7.0: - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + '@jest/reporters@29.7.0': dependencies: '@bcoe/v8-coverage': 0.2.3 '@jest/console': 29.7.0 @@ -600,47 +1962,32 @@ packages: v8-to-istanbul: 9.2.0 transitivePeerDependencies: - supports-color - dev: true - /@jest/schemas@29.6.3: - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 - dev: true - /@jest/source-map@29.6.3: - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/source-map@29.6.3': dependencies: '@jridgewell/trace-mapping': 0.3.22 callsites: 3.1.0 graceful-fs: 4.2.11 - dev: true - /@jest/test-result@29.7.0: - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-result@29.7.0': dependencies: '@jest/console': 29.7.0 '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.2 - dev: true - /@jest/test-sequencer@29.7.0: - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-sequencer@29.7.0': dependencies: '@jest/test-result': 29.7.0 graceful-fs: 4.2.11 jest-haste-map: 29.7.0 slash: 3.0.0 - dev: true - /@jest/transform@29.7.0: - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/transform@29.7.0': dependencies: '@babel/core': 7.23.9 '@jest/types': 29.6.3 @@ -659,11 +2006,8 @@ packages: write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color - dev: true - /@jest/types@29.6.3: - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/types@29.6.3': dependencies: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 @@ -671,176 +2015,105 @@ packages: '@types/node': 20.12.4 '@types/yargs': 17.0.32 chalk: 4.1.2 - dev: true - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.3': dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.22 - dev: true - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - dev: true + '@jridgewell/resolve-uri@3.1.1': {} - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: true + '@jridgewell/set-array@1.1.2': {} - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true + '@jridgewell/sourcemap-codec@1.4.15': {} - /@jridgewell/trace-mapping@0.3.22: - resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + '@jridgewell/trace-mapping@0.3.22': dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 - dev: true - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true + '@nodelib/fs.stat@2.0.5': {} - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - dev: true - /@pkgr/core@0.1.1: - resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - dev: true + '@pkgr/core@0.1.1': {} - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true + '@sinclair/typebox@0.27.8': {} - /@sinonjs/commons@3.0.1: - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 - dev: true - /@sinonjs/fake-timers@10.3.0: - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@sinonjs/fake-timers@10.3.0': dependencies: '@sinonjs/commons': 3.0.1 - dev: true - /@types/babel__core@7.20.5: - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.23.9 '@babel/types': 7.23.9 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.5 - dev: true - /@types/babel__generator@7.6.8: - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@types/babel__generator@7.6.8': dependencies: '@babel/types': 7.23.9 - dev: true - /@types/babel__template@7.4.4: - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - dev: true - /@types/babel__traverse@7.20.5: - resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + '@types/babel__traverse@7.20.5': dependencies: '@babel/types': 7.23.9 - dev: true - /@types/graceful-fs@4.1.9: - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 20.12.4 - dev: true - /@types/istanbul-lib-coverage@2.0.6: - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - dev: true + '@types/istanbul-lib-coverage@2.0.6': {} - /@types/istanbul-lib-report@3.0.3: - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + '@types/istanbul-lib-report@3.0.3': dependencies: '@types/istanbul-lib-coverage': 2.0.6 - dev: true - /@types/istanbul-reports@3.0.4: - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/istanbul-reports@3.0.4': dependencies: '@types/istanbul-lib-report': 3.0.3 - dev: true - /@types/jest@29.5.12: - resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + '@types/jest@29.5.12': dependencies: expect: 29.7.0 pretty-format: 29.7.0 - dev: true - /@types/json-schema@7.0.15: - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: true + '@types/json-schema@7.0.15': {} - /@types/node@20.12.4: - resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==} + '@types/node@20.12.4': dependencies: undici-types: 5.26.5 - dev: true - /@types/semver@7.5.8: - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - dev: true + '@types/semver@7.5.8': {} - /@types/stack-utils@2.0.3: - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - dev: true + '@types/stack-utils@2.0.3': {} - /@types/yargs-parser@21.0.3: - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - dev: true + '@types/yargs-parser@21.0.3': {} - /@types/yargs@17.0.32: - resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + '@types/yargs@17.0.32': dependencies: '@types/yargs-parser': 21.0.3 - dev: true - /@typescript-eslint/eslint-plugin@7.5.0(@typescript-eslint/parser@7.5.0)(eslint@8.57.0)(typescript@5.4.4): - resolution: {integrity: sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/eslint-plugin@7.5.0(@typescript-eslint/parser@7.5.0)(eslint@8.57.0)(typescript@5.4.4)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 7.5.0(eslint@8.57.0)(typescript@5.4.4) @@ -858,17 +2131,8 @@ packages: typescript: 5.4.4 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.4): - resolution: {integrity: sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.4)': dependencies: '@typescript-eslint/scope-manager': 7.5.0 '@typescript-eslint/types': 7.5.0 @@ -879,25 +2143,13 @@ packages: typescript: 5.4.4 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/scope-manager@7.5.0: - resolution: {integrity: sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@7.5.0': dependencies: '@typescript-eslint/types': 7.5.0 '@typescript-eslint/visitor-keys': 7.5.0 - dev: true - /@typescript-eslint/type-utils@7.5.0(eslint@8.57.0)(typescript@5.4.4): - resolution: {integrity: sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/type-utils@7.5.0(eslint@8.57.0)(typescript@5.4.4)': dependencies: '@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.4) '@typescript-eslint/utils': 7.5.0(eslint@8.57.0)(typescript@5.4.4) @@ -907,21 +2159,10 @@ packages: typescript: 5.4.4 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/types@7.5.0: - resolution: {integrity: sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==} - engines: {node: ^18.18.0 || >=20.0.0} - dev: true + '@typescript-eslint/types@7.5.0': {} - /@typescript-eslint/typescript-estree@7.5.0(typescript@5.4.4): - resolution: {integrity: sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/typescript-estree@7.5.0(typescript@5.4.4)': dependencies: '@typescript-eslint/types': 7.5.0 '@typescript-eslint/visitor-keys': 7.5.0 @@ -934,13 +2175,8 @@ packages: typescript: 5.4.4 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/utils@7.5.0(eslint@8.57.0)(typescript@5.4.4): - resolution: {integrity: sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 + '@typescript-eslint/utils@7.5.0(eslint@8.57.0)(typescript@5.4.4)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 @@ -953,102 +2189,57 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: true - /@typescript-eslint/visitor-keys@7.5.0: - resolution: {integrity: sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@7.5.0': dependencies: '@typescript-eslint/types': 7.5.0 eslint-visitor-keys: 3.4.3 - dev: true - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true + '@ungap/structured-clone@1.2.0': {} - /acorn-jsx@5.3.2(acorn@8.11.3): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-jsx@5.3.2(acorn@8.11.3): dependencies: acorn: 8.11.3 - dev: true - /acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true + acorn@8.11.3: {} - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - dev: true - /ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 - dev: true - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true + ansi-regex@5.0.1: {} - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 - dev: true - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - dev: true - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true + ansi-styles@5.2.0: {} - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - dev: true - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 - dev: true - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true + argparse@2.0.1: {} - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true + array-union@2.1.0: {} - /babel-jest@29.7.0(@babel/core@7.23.9): - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 + babel-jest@29.7.0(@babel/core@7.23.9): dependencies: '@babel/core': 7.23.9 '@jest/transform': 29.7.0 @@ -1060,11 +2251,8 @@ packages: slash: 3.0.0 transitivePeerDependencies: - supports-color - dev: true - /babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} + babel-plugin-istanbul@6.1.1: dependencies: '@babel/helper-plugin-utils': 7.22.5 '@istanbuljs/load-nyc-config': 1.1.0 @@ -1073,22 +2261,15 @@ packages: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color - dev: true - /babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.23.9 '@babel/types': 7.23.9 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.5 - dev: true - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.9): - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} - peerDependencies: - '@babel/core': ^7.0.0 + babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.9): dependencies: '@babel/core': 7.23.9 '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.9) @@ -1103,172 +2284,97 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.9) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.9) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.9) - dev: true - /babel-preset-jest@29.6.3(@babel/core@7.23.9): - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 + babel-preset-jest@29.6.3(@babel/core@7.23.9): dependencies: '@babel/core': 7.23.9 babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.9) - dev: true - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true + balanced-match@1.0.2: {} - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 - dev: true - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} + braces@3.0.2: dependencies: fill-range: 7.0.1 - dev: true - /browserslist@4.22.3: - resolution: {integrity: sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + browserslist@4.22.3: dependencies: caniuse-lite: 1.0.30001581 electron-to-chromium: 1.4.648 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.3) - dev: true - /bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} + bs-logger@0.2.6: dependencies: fast-json-stable-stringify: 2.1.0 - dev: true - /bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + bser@2.1.1: dependencies: node-int64: 0.4.0 - dev: true - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: true + buffer-from@1.1.2: {} - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true + callsites@3.1.0: {} - /camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - dev: true + camelcase@5.3.1: {} - /camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - dev: true + camelcase@6.3.0: {} - /caniuse-lite@1.0.30001581: - resolution: {integrity: sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==} - dev: true + caniuse-lite@1.0.30001581: {} - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true - /char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - dev: true + char-regex@1.0.2: {} - /ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - dev: true + ci-info@3.9.0: {} - /cjs-module-lexer@1.2.3: - resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} - dev: true + cjs-module-lexer@1.2.3: {} - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true - /co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - dev: true + co@4.6.0: {} - /collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - dev: true + collect-v8-coverage@1.0.2: {} - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@1.9.3: dependencies: color-name: 1.1.3 - dev: true - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + color-convert@2.0.1: dependencies: color-name: 1.1.4 - dev: true - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true + color-name@1.1.3: {} - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true + color-name@1.1.4: {} - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true + concat-map@0.0.1: {} - /convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true + convert-source-map@2.0.0: {} - /create-jest@29.7.0(@types/node@20.12.4): - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true + create-jest@29.7.0(@types/node@20.12.4): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 @@ -1282,151 +2388,70 @@ packages: - babel-plugin-macros - supports-color - ts-node - dev: true - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true - /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.4: dependencies: ms: 2.1.2 - dev: true - /dedent@1.5.1: - resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - dev: true + dedent@1.5.1: {} - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true + deep-is@0.1.4: {} - /deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - dev: true + deepmerge@4.3.1: {} - /detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - dev: true + detect-newline@3.1.0: {} - /diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true + diff-sequences@29.6.3: {} - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dev: true - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + doctrine@3.0.0: dependencies: esutils: 2.0.3 - dev: true - /download-file-sync@1.0.4: - resolution: {integrity: sha512-vH92qNH508jZZA12HQNq/aiMDfagr4JvjFiI17Bi8oYjsxwv5ZVIi7iHkYmUXxOQUr90tcVX+8EPePjAqG1Y0w==} - dev: true + download-file-sync@1.0.4: {} - /electron-to-chromium@1.4.648: - resolution: {integrity: sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg==} - dev: true + electron-to-chromium@1.4.648: {} - /emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - dev: true + emittery@0.13.1: {} - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true + emoji-regex@8.0.0: {} - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 - dev: true - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true + escalade@3.1.1: {} - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: true + escape-string-regexp@1.0.5: {} - /escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - dev: true + escape-string-regexp@2.0.0: {} - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true + escape-string-regexp@4.0.0: {} - /eslint-plugin-prettier@5.1.3(eslint@8.57.0)(prettier@3.2.5): - resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '*' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true + eslint-plugin-prettier@5.1.3(eslint@8.57.0)(prettier@3.2.5): dependencies: eslint: 8.57.0 prettier: 3.2.5 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 - dev: true - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - dev: true - /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + eslint-visitor-keys@3.4.3: {} - /eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + eslint@8.57.0: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@eslint-community/regexpp': 4.10.0 @@ -1468,50 +2493,28 @@ packages: text-table: 0.2.0 transitivePeerDependencies: - supports-color - dev: true - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@9.6.1: dependencies: acorn: 8.11.3 acorn-jsx: 5.3.2(acorn@8.11.3) eslint-visitor-keys: 3.4.3 - dev: true - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: true + esprima@4.0.1: {} - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} + esquery@1.5.0: dependencies: estraverse: 5.3.0 - dev: true - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 - dev: true - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true + estraverse@5.3.0: {} - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true + esutils@2.0.3: {} - /execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} + execa@5.1.1: dependencies: cross-spawn: 7.0.3 get-stream: 6.0.1 @@ -1522,158 +2525,91 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 - dev: true - /exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - dev: true + exit@0.1.2: {} - /expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + expect@29.7.0: dependencies: '@jest/expect-utils': 29.7.0 jest-get-type: 29.6.3 jest-matcher-utils: 29.7.0 jest-message-util: 29.7.0 jest-util: 29.7.0 - dev: true - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true + fast-deep-equal@3.1.3: {} - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true + fast-diff@1.3.0: {} - /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 - dev: true - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true + fast-json-stable-stringify@2.1.0: {} - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true + fast-levenshtein@2.0.6: {} - /fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fastq@1.17.1: dependencies: reusify: 1.0.4 - dev: true - /fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fb-watchman@2.0.2: dependencies: bser: 2.1.1 - dev: true - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 - dev: true - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 - dev: true - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} + find-up@4.1.0: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - dev: true - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - dev: true - /flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@3.2.0: dependencies: flatted: 3.3.1 keyv: 4.5.4 rimraf: 3.0.2 - dev: true - /flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - dev: true + flatted@3.3.1: {} - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true + fs.realpath@1.0.0: {} - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true + fsevents@2.3.3: optional: true - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true + function-bind@1.1.2: {} - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true + gensync@1.0.0-beta.2: {} - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: true + get-caller-file@2.0.5: {} - /get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - dev: true + get-package-type@0.1.0: {} - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: true + get-stream@6.0.1: {} - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - dev: true - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 - dev: true - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + glob@7.2.3: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -1681,23 +2617,14 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true + globals@11.12.0: {} - /globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@13.24.0: dependencies: type-fest: 0.20.2 - dev: true - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + globby@11.1.0: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 @@ -1705,139 +2632,71 @@ packages: ignore: 5.3.1 merge2: 1.4.1 slash: 3.0.0 - dev: true - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true + graceful-fs@4.2.11: {} - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true + graphemer@1.4.0: {} - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: true + has-flag@3.0.0: {} - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true + has-flag@4.0.0: {} - /hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} - engines: {node: '>= 0.4'} + hasown@2.0.0: dependencies: function-bind: 1.1.2 - dev: true - /html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - dev: true + html-escaper@2.0.2: {} - /human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - dev: true + human-signals@2.1.0: {} - /ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - dev: true + ignore@5.3.1: {} - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: true - /import-local@3.1.0: - resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} - engines: {node: '>=8'} - hasBin: true + import-local@3.1.0: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 - dev: true - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true + imurmurhash@0.1.4: {} - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + inflight@1.0.6: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true + inherits@2.0.4: {} - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true + is-arrayish@0.2.1: {} - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.13.1: dependencies: hasown: 2.0.0 - dev: true - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true + is-extglob@2.1.1: {} - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true + is-fullwidth-code-point@3.0.0: {} - /is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - dev: true + is-generator-fn@2.1.0: {} - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - dev: true - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true + is-number@7.0.0: {} - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true + is-path-inside@3.0.3: {} - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: true + is-stream@2.0.1: {} - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true + isexe@2.0.0: {} - /istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - dev: true + istanbul-lib-coverage@3.2.2: {} - /istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} + istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.23.9 '@babel/parser': 7.23.9 @@ -1846,11 +2705,8 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color - dev: true - /istanbul-lib-instrument@6.0.1: - resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} - engines: {node: '>=10'} + istanbul-lib-instrument@6.0.1: dependencies: '@babel/core': 7.23.9 '@babel/parser': 7.23.9 @@ -1859,48 +2715,33 @@ packages: semver: 7.5.4 transitivePeerDependencies: - supports-color - dev: true - /istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - dev: true - /istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} + istanbul-lib-source-maps@4.0.1: dependencies: debug: 4.3.4 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: - supports-color - dev: true - /istanbul-reports@3.1.6: - resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} - engines: {node: '>=8'} + istanbul-reports@3.1.6: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - dev: 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} + jest-changed-files@29.7.0: dependencies: execa: 5.1.1 jest-util: 29.7.0 p-limit: 3.1.0 - dev: true - /jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-circus@29.7.0: dependencies: '@jest/environment': 29.7.0 '@jest/expect': 29.7.0 @@ -1925,17 +2766,8 @@ packages: transitivePeerDependencies: - babel-plugin-macros - supports-color - dev: true - /jest-cli@29.7.0(@types/node@20.12.4): - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + jest-cli@29.7.0(@types/node@20.12.4): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 @@ -1953,19 +2785,8 @@ packages: - babel-plugin-macros - supports-color - ts-node - dev: true - /jest-config@29.7.0(@types/node@20.12.4): - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true + jest-config@29.7.0(@types/node@20.12.4): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 @@ -1993,39 +2814,27 @@ packages: transitivePeerDependencies: - babel-plugin-macros - supports-color - dev: true - /jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-diff@29.7.0: dependencies: chalk: 4.1.2 diff-sequences: 29.6.3 jest-get-type: 29.6.3 pretty-format: 29.7.0 - dev: true - /jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-docblock@29.7.0: dependencies: detect-newline: 3.1.0 - dev: true - /jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-each@29.7.0: dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 jest-get-type: 29.6.3 jest-util: 29.7.0 pretty-format: 29.7.0 - dev: true - /jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-environment-node@29.7.0: dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 @@ -2033,16 +2842,10 @@ packages: '@types/node': 20.12.4 jest-mock: 29.7.0 jest-util: 29.7.0 - dev: true - /jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true + jest-get-type@29.6.3: {} - /jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-haste-map@29.7.0: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 @@ -2057,29 +2860,20 @@ packages: walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 - dev: true - /jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-leak-detector@29.7.0: dependencies: jest-get-type: 29.6.3 pretty-format: 29.7.0 - dev: true - /jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-matcher-utils@29.7.0: dependencies: chalk: 4.1.2 jest-diff: 29.7.0 jest-get-type: 29.6.3 pretty-format: 29.7.0 - dev: true - /jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-message-util@29.7.0: dependencies: '@babel/code-frame': 7.23.5 '@jest/types': 29.6.3 @@ -2090,47 +2884,27 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 - dev: true - /jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 '@types/node': 20.12.4 jest-util: 29.7.0 - dev: true - /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): dependencies: jest-resolve: 29.7.0 - dev: true - /jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true + jest-regex-util@29.6.3: {} - /jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve-dependencies@29.7.0: dependencies: jest-regex-util: 29.6.3 jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color - dev: true - /jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve@29.7.0: dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 @@ -2141,11 +2915,8 @@ packages: resolve: 1.22.8 resolve.exports: 2.0.2 slash: 3.0.0 - dev: true - /jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runner@29.7.0: dependencies: '@jest/console': 29.7.0 '@jest/environment': 29.7.0 @@ -2170,11 +2941,8 @@ packages: source-map-support: 0.5.13 transitivePeerDependencies: - supports-color - dev: true - /jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runtime@29.7.0: dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 @@ -2200,11 +2968,8 @@ packages: strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - dev: true - /jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-snapshot@29.7.0: dependencies: '@babel/core': 7.23.9 '@babel/generator': 7.23.6 @@ -2228,11 +2993,8 @@ packages: semver: 7.5.4 transitivePeerDependencies: - supports-color - dev: true - /jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 '@types/node': 20.12.4 @@ -2240,11 +3002,8 @@ packages: ci-info: 3.9.0 graceful-fs: 4.2.11 picomatch: 2.3.1 - dev: true - /jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-validate@29.7.0: dependencies: '@jest/types': 29.6.3 camelcase: 6.3.0 @@ -2252,11 +3011,8 @@ packages: jest-get-type: 29.6.3 leven: 3.1.0 pretty-format: 29.7.0 - dev: true - /jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-watcher@29.7.0: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 @@ -2266,27 +3022,15 @@ packages: emittery: 0.13.1 jest-util: 29.7.0 string-length: 4.0.2 - dev: true - /jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-worker@29.7.0: dependencies: '@types/node': 20.12.4 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - dev: true - /jest@29.7.0(@types/node@20.12.4): - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + jest@29.7.0(@types/node@20.12.4): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 @@ -2297,218 +3041,119 @@ packages: - babel-plugin-macros - supports-color - ts-node - dev: true - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true + js-tokens@4.0.0: {} - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true + js-yaml@3.14.1: dependencies: argparse: 1.0.10 esprima: 4.0.1 - dev: true - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true + js-yaml@4.1.0: dependencies: argparse: 2.0.1 - dev: true - /jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - dev: true + jsesc@2.5.2: {} - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true + json-buffer@3.0.1: {} - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true + json-parse-even-better-errors@2.3.1: {} - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true + json-schema-traverse@0.4.1: {} - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true + json-stable-stringify-without-jsonify@1.0.1: {} - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - dev: true + json5@2.2.3: {} - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 - dev: true - /kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - dev: true + kleur@3.0.3: {} - /lemmy-js-client@0.19.4-alpha.16: - resolution: {integrity: sha512-9BKCpZeH5+dDkSuYLVPvJnRGOSa3/jBUqYlQH3r1p8TyCCBrxstIC2I+h9dZZtOg4RmK7ShcuZdk9LSMe1ZMyw==} - dev: true + lemmy-js-client@0.19.4-alpha.18: {} - /leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - dev: true + leven@3.1.0: {} - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true + lines-and-columns@1.2.4: {} - /locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 - dev: true - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 - dev: true - /lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - dev: true + lodash.memoize@4.1.2: {} - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true + lodash.merge@4.6.2: {} - /lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 - dev: true - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 - dev: true - /make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} + make-dir@4.0.0: dependencies: semver: 7.5.4 - dev: true - /make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true + make-error@1.3.6: {} - /makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + makeerror@1.0.12: dependencies: tmpl: 1.0.5 - dev: true - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true + merge-stream@2.0.0: {} - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true + merge2@1.4.1: {} - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} + micromatch@4.0.5: dependencies: braces: 3.0.2 picomatch: 2.3.1 - dev: true - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: true + mimic-fn@2.1.0: {} - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - dev: true - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.3: dependencies: brace-expansion: 2.0.1 - dev: true - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true + ms@2.1.2: {} - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true + natural-compare@1.4.0: {} - /node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - dev: true + node-int64@0.4.0: {} - /node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - dev: true + node-releases@2.0.14: {} - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true + normalize-path@3.0.0: {} - /npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 - dev: true - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + once@1.4.0: dependencies: wrappy: 1.0.2 - dev: true - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 - dev: true - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} + optionator@0.9.3: dependencies: '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 @@ -2516,409 +3161,207 @@ packages: levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true - /p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + p-limit@2.3.0: dependencies: p-try: 2.2.0 - dev: true - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - dev: true - /p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + p-locate@4.1.0: dependencies: p-limit: 2.3.0 - dev: true - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + p-locate@5.0.0: dependencies: p-limit: 3.1.0 - dev: true - /p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - dev: true + p-try@2.2.0: {} - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - dev: true - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.23.5 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true + path-exists@4.0.0: {} - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true + path-is-absolute@1.0.1: {} - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true + path-key@3.1.1: {} - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true + path-parse@1.0.7: {} - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true + path-type@4.0.0: {} - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true + picocolors@1.0.0: {} - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true + picomatch@2.3.1: {} - /pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - dev: true + pirates@4.0.6: {} - /pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 - dev: true - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true + prelude-ls@1.2.1: {} - /prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} + prettier-linter-helpers@1.0.0: dependencies: fast-diff: 1.3.0 - dev: true - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} - engines: {node: '>=14'} - hasBin: true - dev: true + prettier@3.2.5: {} - /pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 - dev: true - /prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} + prompts@2.4.2: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 - dev: true - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - dev: true + punycode@2.3.1: {} - /pure-rand@6.0.4: - resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} - dev: true + pure-rand@6.0.4: {} - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true + queue-microtask@1.2.3: {} - /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true + react-is@18.2.0: {} - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: true + require-directory@2.1.1: {} - /resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 - dev: true - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true + resolve-from@4.0.0: {} - /resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - dev: true + resolve-from@5.0.0: {} - /resolve.exports@2.0.2: - resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} - engines: {node: '>=10'} - dev: true + resolve.exports@2.0.2: {} - /resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true + resolve@1.22.8: dependencies: is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true + reusify@1.0.4: {} - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true + rimraf@3.0.2: dependencies: glob: 7.2.3 - dev: true - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - dev: true - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true + semver@6.3.1: {} - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true + semver@7.5.4: dependencies: lru-cache: 6.0.0 - dev: true - /semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} - engines: {node: '>=10'} - hasBin: true + semver@7.6.0: dependencies: lru-cache: 6.0.0 - dev: true - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - dev: true - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true + shebang-regex@3.0.0: {} - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true + signal-exit@3.0.7: {} - /sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: true + sisteransi@1.0.5: {} - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true + slash@3.0.0: {} - /source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - dev: true - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true + source-map@0.6.1: {} - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true + sprintf-js@1.0.3: {} - /stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 - dev: true - /string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} + string-length@4.0.2: dependencies: char-regex: 1.0.2 strip-ansi: 6.0.1 - dev: true - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - dev: true - /strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - dev: true + strip-bom@4.0.0: {} - /strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - dev: true + strip-final-newline@2.0.0: {} - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true + strip-json-comments@3.1.1: {} - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 - dev: true - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 - dev: true - /supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} + supports-color@8.1.1: dependencies: has-flag: 4.0.0 - dev: true - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true + supports-preserve-symlinks-flag@1.0.0: {} - /synckit@0.8.8: - resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} - engines: {node: ^14.18.0 || >=16.0.0} + synckit@0.8.8: dependencies: '@pkgr/core': 0.1.1 tslib: 2.6.2 - dev: true - /test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 minimatch: 3.1.2 - dev: true - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true + text-table@0.2.0: {} - /tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - dev: true + tmpl@1.0.5: {} - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - dev: true + to-fast-properties@2.0.0: {} - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - dev: true - /ts-api-utils@1.3.0(typescript@5.4.4): - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' + ts-api-utils@1.3.0(typescript@5.4.4): dependencies: typescript: 5.4.4 - dev: true - /ts-jest@29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.4.4): - resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} - engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true + ts-jest@29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.4.4): dependencies: '@babel/core': 7.23.9 bs-logger: 0.2.6 @@ -2931,126 +3374,69 @@ packages: semver: 7.5.4 typescript: 5.4.4 yargs-parser: 21.1.1 - dev: true - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: true + tslib@2.6.2: {} - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - dev: true - /type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - dev: true + type-detect@4.0.8: {} - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true + type-fest@0.20.2: {} - /type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - dev: true + type-fest@0.21.3: {} - /typescript@5.4.4: - resolution: {integrity: sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true + typescript@5.4.4: {} - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true + undici-types@5.26.5: {} - /update-browserslist-db@1.0.13(browserslist@4.22.3): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + update-browserslist-db@1.0.13(browserslist@4.22.3): dependencies: browserslist: 4.22.3 escalade: 3.1.1 picocolors: 1.0.0 - dev: true - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uri-js@4.4.1: dependencies: punycode: 2.3.1 - dev: true - /v8-to-istanbul@9.2.0: - resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} - engines: {node: '>=10.12.0'} + v8-to-istanbul@9.2.0: dependencies: '@jridgewell/trace-mapping': 0.3.22 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - dev: true - /walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + walker@1.0.8: dependencies: makeerror: 1.0.12 - dev: true - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + which@2.0.2: dependencies: isexe: 2.0.0 - dev: true - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true + wrappy@1.0.2: {} - /write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@4.0.2: dependencies: imurmurhash: 0.1.4 signal-exit: 3.0.7 - dev: true - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true + y18n@5.0.8: {} - /yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true + yallist@3.1.1: {} - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true + yallist@4.0.0: {} - /yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - dev: true + yargs-parser@21.1.1: {} - /yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + yargs@17.7.2: dependencies: cliui: 8.0.1 escalade: 3.1.1 @@ -3059,9 +3445,5 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true + yocto-queue@0.1.0: {} diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh index 147a553ab..31eb111c2 100755 --- a/api_tests/prepare-drone-federation-test.sh +++ b/api_tests/prepare-drone-federation-test.sh @@ -3,19 +3,19 @@ # it is expected that this script is called by run-federation-test.sh script. set -e -if [ -n "$LEMMY_LOG_LEVEL" ]; +if [ -z "$LEMMY_LOG_LEVEL" ]; then - LEMMY_LOG_LEVEL=warn + LEMMY_LOG_LEVEL=info fi export RUST_BACKTRACE=1 -#export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL" +export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL" export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min # pictrs setup if [ ! -f "api_tests/pict-rs" ]; then - curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs + curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/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/comment.spec.ts b/api_tests/src/comment.spec.ts index fc547481c..dfab4109c 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -45,7 +45,6 @@ let postOnAlphaRes: PostResponse; beforeAll(async () => { await setupLogins(); - await unfollows(); await Promise.all([followBeta(alpha), followBeta(gamma)]); betaCommunity = (await resolveBetaCommunity(alpha)).community; if (betaCommunity) { diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index aa96f563c..d172f7045 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -242,7 +242,7 @@ test("Admin actions in remote community are not federated to origin", async () = ); expect(banRes.banned).toBe(true); - // ban doesnt federate to community's origin instance alpha + // ban doesn't federate to community's origin instance alpha let alphaPost = (await resolvePost(alpha, gammaPost.post)).post; expect(alphaPost?.creator_banned_from_community).toBe(false); @@ -380,8 +380,8 @@ test("User blocks instance, communities are hidden", async () => { test("Community follower count is federated", async () => { // Follow the beta community from alpha let community = await createCommunity(beta); - let community_id = community.community_view.community.actor_id; - let resolved = await resolveCommunity(alpha, community_id); + let communityActorId = community.community_view.community.actor_id; + let resolved = await resolveCommunity(alpha, communityActorId); if (!resolved.community) { throw "Missing beta community"; } @@ -389,7 +389,7 @@ test("Community follower count is federated", async () => { await followCommunity(alpha, true, resolved.community.community.id); let followed = ( await waitUntil( - () => resolveCommunity(alpha, community_id), + () => resolveCommunity(alpha, communityActorId), c => c.community?.subscribed === "Subscribed", ) ).community; @@ -398,7 +398,7 @@ test("Community follower count is federated", async () => { expect(followed?.counts.subscribers).toBe(1); // Follow the community from gamma - resolved = await resolveCommunity(gamma, community_id); + resolved = await resolveCommunity(gamma, communityActorId); if (!resolved.community) { throw "Missing beta community"; } @@ -406,7 +406,7 @@ test("Community follower count is federated", async () => { await followCommunity(gamma, true, resolved.community.community.id); followed = ( await waitUntil( - () => resolveCommunity(gamma, community_id), + () => resolveCommunity(gamma, communityActorId), c => c.community?.subscribed === "Subscribed", ) ).community; @@ -415,7 +415,7 @@ test("Community follower count is federated", async () => { expect(followed?.counts?.subscribers).toBe(2); // Follow the community from delta - resolved = await resolveCommunity(delta, community_id); + resolved = await resolveCommunity(delta, communityActorId); if (!resolved.community) { throw "Missing beta community"; } @@ -423,7 +423,7 @@ test("Community follower count is federated", async () => { await followCommunity(delta, true, resolved.community.community.id); followed = ( await waitUntil( - () => resolveCommunity(delta, community_id), + () => resolveCommunity(delta, communityActorId), c => c.community?.subscribed === "Subscribed", ) ).community; @@ -452,7 +452,7 @@ test("Dont receive community activities after unsubscribe", async () => { ); expect(communityRes1.community_view.counts.subscribers).toBe(2); - // temporarily block alpha, so that it doesnt know about unfollow + // temporarily block alpha, so that it doesn't know about unfollow let editSiteForm: EditSite = {}; editSiteForm.allowed_instances = ["lemmy-epsilon"]; await beta.editSite(editSiteForm); @@ -513,7 +513,7 @@ test("Fetch community, includes posts", async () => { expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id); }); -test("Content in local-only community doesnt federate", async () => { +test("Content in local-only community doesn't federate", async () => { // create a community and set it local-only let communityRes = (await createCommunity(alpha)).community_view.community; let form: EditCommunity = { diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index 055c498e0..123982e85 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -27,21 +27,25 @@ import { setupLogins, waitForPost, unfollows, - editPostThumbnail, getPost, waitUntil, + createPostWithThumbnail, + sampleImage, + sampleSite, } from "./shared"; const downloadFileSync = require("download-file-sync"); beforeAll(setupLogins); -afterAll(unfollows); +afterAll(async () => { + await Promise.all([unfollows(), deleteAllImages(alpha)]); +}); test("Upload image and delete it", async () => { // Before running this test, you need to delete all previous images in the DB await deleteAllImages(alpha); - // Upload test image. We use a simple string buffer as pictrs doesnt require an actual image + // Upload test image. We use a simple string buffer as pictrs doesn't require an actual image // in testing mode. const upload_form: UploadImage = { image: Buffer.from("test"), @@ -71,9 +75,14 @@ test("Upload image and delete it", async () => { // The deleteUrl is a combination of the endpoint, delete token, and alias let firstImage = listMediaRes.images[0]; - let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.pictrs_delete_token}/${firstImage.pictrs_alias}`; + let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.local_image.pictrs_delete_token}/${firstImage.local_image.pictrs_alias}`; expect(deleteUrl).toBe(upload.delete_url); + // Make sure the uploader is correct + expect(firstImage.person.actor_id).toBe( + `http://lemmy-alpha:8541/u/lemmy_alpha`, + ); + // delete image const delete_form: DeleteImage = { token: upload.files![0].delete_token, @@ -153,7 +162,6 @@ test("Purge post, linked image removed", async () => { expect(post.post_view.post.url).toBe(upload.url); // purge post - const purgeForm: PurgePost = { post_id: post.post_view.post.id, }; @@ -165,48 +173,94 @@ test("Purge post, linked image removed", async () => { expect(content2).toBe(""); }); -test("Images in remote post are proxied if setting enabled", async () => { - let user = await registerUser(beta, betaUrl); +test("Images in remote image post are proxied if setting enabled", async () => { let community = await createCommunity(gamma); - - const upload_form: UploadImage = { - image: Buffer.from("test"), - }; - const upload = await user.uploadImage(upload_form); - let post = await createPost( + let postRes = await createPost( gamma, community.community_view.community.id, - upload.url, - "![](http://example.com/image2.png)", + sampleImage, + `![](${sampleImage})`, ); - expect(post.post_view.post).toBeDefined(); + const post = postRes.post_view.post; + expect(post).toBeDefined(); // remote image gets proxied after upload expect( - post.post_view.post.url?.startsWith( + post.thumbnail_url?.startsWith( "http://lemmy-gamma:8561/api/v3/image_proxy?url", ), ).toBeTruthy(); expect( - post.post_view.post.body?.startsWith( - "![](http://lemmy-gamma:8561/api/v3/image_proxy?url", - ), + post.body?.startsWith("![](http://lemmy-gamma:8561/api/v3/image_proxy?url"), ).toBeTruthy(); - let epsilonPost = await resolvePost(epsilon, post.post_view.post); - expect(epsilonPost.post).toBeDefined(); + // Make sure that it ends with jpg, to be sure its an image + expect(post.thumbnail_url?.endsWith(".jpg")).toBeTruthy(); + + let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post); + expect(epsilonPostRes.post).toBeDefined(); + + // Fetch the post again, the metadata should be backgrounded now + // Wait for the metadata to get fetched, since this is backgrounded now + let epsilonPostRes2 = await waitUntil( + () => getPost(epsilon, epsilonPostRes.post!.post.id), + p => p.post_view.post.thumbnail_url != undefined, + ); + const epsilonPost = epsilonPostRes2.post_view.post; - // remote image gets proxied after federation expect( - epsilonPost.post!.post.url?.startsWith( + epsilonPost.thumbnail_url?.startsWith( "http://lemmy-epsilon:8581/api/v3/image_proxy?url", ), ).toBeTruthy(); expect( - epsilonPost.post!.post.body?.startsWith( + epsilonPost.body?.startsWith( "![](http://lemmy-epsilon:8581/api/v3/image_proxy?url", ), ).toBeTruthy(); + + // Make sure that it ends with jpg, to be sure its an image + expect(epsilonPost.thumbnail_url?.endsWith(".jpg")).toBeTruthy(); +}); + +test("Thumbnail of remote image link is proxied if setting enabled", async () => { + let community = await createCommunity(gamma); + let postRes = await createPost( + gamma, + community.community_view.community.id, + // The sample site metadata thumbnail ends in png + sampleSite, + ); + const post = postRes.post_view.post; + expect(post).toBeDefined(); + + // remote image gets proxied after upload + expect( + post.thumbnail_url?.startsWith( + "http://lemmy-gamma:8561/api/v3/image_proxy?url", + ), + ).toBeTruthy(); + + // Make sure that it ends with png, to be sure its an image + expect(post.thumbnail_url?.endsWith(".png")).toBeTruthy(); + + let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post); + expect(epsilonPostRes.post).toBeDefined(); + + let epsilonPostRes2 = await waitUntil( + () => getPost(epsilon, epsilonPostRes.post!.post.id), + p => p.post_view.post.thumbnail_url != undefined, + ); + const epsilonPost = epsilonPostRes2.post_view.post; + + expect( + epsilonPost.thumbnail_url?.startsWith( + "http://lemmy-epsilon:8581/api/v3/image_proxy?url", + ), + ).toBeTruthy(); + + // Make sure that it ends with png, to be sure its an image + expect(epsilonPost.thumbnail_url?.endsWith(".png")).toBeTruthy(); }); test("No image proxying if setting is disabled", async () => { @@ -226,15 +280,15 @@ test("No image proxying if setting is disabled", async () => { alpha, community.community_view.community.id, upload.url, - "![](http://example.com/image2.png)", + `![](${sampleImage})`, ); expect(post.post_view.post).toBeDefined(); - // remote image doesnt get proxied after upload + // remote image doesn't get proxied after upload expect( post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"), ).toBeTruthy(); - expect(post.post_view.post.body).toBe("![](http://example.com/image2.png)"); + expect(post.post_view.post.body).toBe(`![](${sampleImage})`); let betaPost = await waitForPost( beta, @@ -243,12 +297,11 @@ test("No image proxying if setting is disabled", async () => { ); expect(betaPost.post).toBeDefined(); - // remote image doesnt get proxied after federation + // remote image doesn't get proxied after federation expect( betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"), ).toBeTruthy(); - expect(betaPost.post.body).toBe("![](http://example.com/image2.png)"); - + expect(betaPost.post.body).toBe(`![](${sampleImage})`); // Make sure the alt text got federated expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text); }); @@ -264,10 +317,11 @@ test("Make regular post, and give it a custom thumbnail", async () => { // Use wikipedia since it has an opengraph image const wikipediaUrl = "https://wikipedia.org/"; - let post = await createPost( + let post = await createPostWithThumbnail( alphaImage, community.community_view.community.id, wikipediaUrl, + upload1.url!, ); // Wait for the metadata to get fetched, since this is backgrounded now @@ -276,21 +330,11 @@ test("Make regular post, and give it a custom thumbnail", async () => { p => p.post_view.post.thumbnail_url != undefined, ); expect(post.post_view.post.url).toBe(wikipediaUrl); - expect(post.post_view.post.thumbnail_url).toBeDefined(); - - // Edit the thumbnail - await editPostThumbnail(alphaImage, post.post_view.post, upload1.url!); - - post = await waitUntil( - () => getPost(alphaImage, post.post_view.post.id), - p => p.post_view.post.thumbnail_url == upload1.url, - ); - - // Make sure the thumbnail got edited. + // Make sure it uses custom thumbnail expect(post.post_view.post.thumbnail_url).toBe(upload1.url); }); -test("Create an image post, and make sure a custom thumbnail doesnt overwrite it", async () => { +test("Create an image post, and make sure a custom thumbnail doesn't overwrite it", async () => { const uploadForm1: UploadImage = { image: Buffer.from("test1"), }; @@ -303,23 +347,17 @@ test("Create an image post, and make sure a custom thumbnail doesnt overwrite it const community = await createCommunity(alphaImage); - let post = await createPost( + let post = await createPostWithThumbnail( alphaImage, community.community_view.community.id, - upload1.url, + upload1.url!, + upload2.url!, ); - expect(post.post_view.post.url).toBe(upload1.url); - - // Edit the post - await editPostThumbnail(alphaImage, post.post_view.post, upload2.url!); - - // Wait for the metadata to get fetched post = await waitUntil( () => getPost(alphaImage, post.post_view.post.id), - p => p.post_view.post.thumbnail_url == upload1.url, + p => p.post_view.post.thumbnail_url != undefined, ); - - // Make sure the new custom thumbnail is ignored, and doesn't overwrite the image post expect(post.post_view.post.url).toBe(upload1.url); - expect(post.post_view.post.thumbnail_url).toBe(upload1.url); + // Make sure the custom thumbnail is ignored + expect(post.post_view.post.thumbnail_url == upload2.url).toBe(false); }); diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index b2ab50222..cb45274c6 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -48,7 +48,6 @@ beforeAll(async () => { await setupLogins(); betaCommunity = (await resolveBetaCommunity(alpha)).community; expect(betaCommunity).toBeDefined(); - await unfollows(); }); afterAll(unfollows); @@ -83,10 +82,7 @@ async function assertPostFederation(postOne: PostView, postTwo: PostView) { test("Create a post", async () => { // Setup some allowlists and blocklists - let editSiteForm: EditSite = { - allowed_instances: ["lemmy-beta"], - }; - await delta.editSite(editSiteForm); + const editSiteForm: EditSite = {}; editSiteForm.allowed_instances = []; editSiteForm.blocked_instances = ["lemmy-alpha"]; @@ -661,40 +657,60 @@ test("A and G subscribe to B (center) A posts, it gets announced to G", async () }); test("Report a post", async () => { - // Note, this is a different one from the setup - let betaCommunity = (await resolveBetaCommunity(beta)).community; - if (!betaCommunity) { - throw "Missing beta community"; - } + // Create post from alpha + let alphaCommunity = (await resolveBetaCommunity(alpha)).community!; await followBeta(alpha); - let postRes = await createPost(beta, betaCommunity.community.id); + let postRes = await createPost(alpha, alphaCommunity.community.id); expect(postRes.post_view.post).toBeDefined(); let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post; if (!alphaPost) { throw "Missing alpha post"; } - let alphaReport = ( - await reportPost(alpha, alphaPost.post.id, randomString(10)) - ).post_report_view.post_report; + // Send report from gamma + let gammaPost = (await resolvePost(gamma, alphaPost.post)).post!; + let gammaReport = ( + await reportPost(gamma, gammaPost.post.id, randomString(10)) + ).post_report_view.post_report; + expect(gammaReport).toBeDefined(); + + // Report was federated to community instance let betaReport = (await waitUntil( () => listPostReports(beta).then(p => p.post_reports.find( r => - r.post_report.original_post_name === alphaReport.original_post_name, + r.post_report.original_post_name === gammaReport.original_post_name, ), ), res => !!res, ))!.post_report; expect(betaReport).toBeDefined(); expect(betaReport.resolved).toBe(false); - expect(betaReport.original_post_name).toBe(alphaReport.original_post_name); - expect(betaReport.original_post_url).toBe(alphaReport.original_post_url); - expect(betaReport.original_post_body).toBe(alphaReport.original_post_body); - expect(betaReport.reason).toBe(alphaReport.reason); + expect(betaReport.original_post_name).toBe(gammaReport.original_post_name); + //expect(betaReport.original_post_url).toBe(gammaReport.original_post_url); + expect(betaReport.original_post_body).toBe(gammaReport.original_post_body); + expect(betaReport.reason).toBe(gammaReport.reason); await unfollowRemotes(alpha); + + // Report was federated to poster's instance + let alphaReport = (await waitUntil( + () => + listPostReports(alpha).then(p => + p.post_reports.find( + r => + r.post_report.original_post_name === gammaReport.original_post_name, + ), + ), + res => !!res, + ))!.post_report; + expect(alphaReport).toBeDefined(); + expect(alphaReport.resolved).toBe(false); + expect(alphaReport.original_post_name).toBe(gammaReport.original_post_name); + //expect(alphaReport.original_post_url).toBe(gammaReport.original_post_url); + expect(alphaReport.original_post_body).toBe(gammaReport.original_post_body); + expect(alphaReport.reason).toBe(gammaReport.reason); }); test("Fetch post via redirect", async () => { @@ -729,7 +745,7 @@ test("Block post that contains banned URL", async () => { await epsilon.editSite(editSiteForm); - await delay(500); + await delay(); if (!betaCommunity) { throw "Missing beta community"; diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index e363fffa4..056f25538 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -81,21 +81,24 @@ import { ListingType } from "lemmy-js-client/dist/types/ListingType"; export const fetchFunction = fetch; export const imageFetchLimit = 50; +export const sampleImage = + "https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg"; +export const sampleSite = "https://yahoo.com"; -export let alphaUrl = "http://127.0.0.1:8541"; -export let betaUrl = "http://127.0.0.1:8551"; -export let gammaUrl = "http://127.0.0.1:8561"; -export let deltaUrl = "http://127.0.0.1:8571"; -export let epsilonUrl = "http://127.0.0.1:8581"; +export const alphaUrl = "http://127.0.0.1:8541"; +export const betaUrl = "http://127.0.0.1:8551"; +export const gammaUrl = "http://127.0.0.1:8561"; +export const deltaUrl = "http://127.0.0.1:8571"; +export const epsilonUrl = "http://127.0.0.1:8581"; -export let alpha = new LemmyHttp(alphaUrl, { fetchFunction }); -export let alphaImage = new LemmyHttp(alphaUrl); -export let beta = new LemmyHttp(betaUrl, { fetchFunction }); -export let gamma = new LemmyHttp(gammaUrl, { fetchFunction }); -export let delta = new LemmyHttp(deltaUrl, { fetchFunction }); -export let epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); +export const alpha = new LemmyHttp(alphaUrl, { fetchFunction }); +export const alphaImage = new LemmyHttp(alphaUrl); +export const beta = new LemmyHttp(betaUrl, { fetchFunction }); +export const gamma = new LemmyHttp(gammaUrl, { fetchFunction }); +export const delta = new LemmyHttp(deltaUrl, { fetchFunction }); +export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); -export let betaAllowedInstances = [ +export const betaAllowedInstances = [ "lemmy-alpha", "lemmy-gamma", "lemmy-delta", @@ -180,6 +183,10 @@ export async function setupLogins() { ]; await gamma.editSite(editSiteForm); + // Setup delta allowed instance + editSiteForm.allowed_instances = ["lemmy-beta"]; + await delta.editSite(editSiteForm); + // Create the main alpha/beta communities // Ignore thrown errors of duplicates try { @@ -203,6 +210,7 @@ export async function createPost( // use example.com for consistent title and embed description name: string = randomString(5), alt_text = randomString(10), + custom_thumbnail: string | undefined = undefined, ): Promise { let form: CreatePost = { name, @@ -210,6 +218,7 @@ export async function createPost( body, alt_text, community_id, + custom_thumbnail, }; return api.createPost(form); } @@ -226,16 +235,19 @@ export async function editPost( return api.editPost(form); } -export async function editPostThumbnail( +export async function createPostWithThumbnail( api: LemmyHttp, - post: Post, - customThumbnail: string, + community_id: number, + url: string, + custom_thumbnail: string, ): Promise { - let form: EditPost = { - post_id: post.id, - custom_thumbnail: customThumbnail, + let form: CreatePost = { + name: randomString(10), + url, + community_id, + custom_thumbnail, }; - return api.editPost(form); + return api.createPost(form); } export async function deletePost( @@ -688,8 +700,8 @@ export async function saveUserSettingsBio( export async function saveUserSettingsFederated( api: LemmyHttp, ): Promise { - let avatar = "https://image.flaticon.com/icons/png/512/35/35896.png"; - let banner = "https://image.flaticon.com/icons/png/512/36/35896.png"; + let avatar = sampleImage; + let banner = sampleImage; let bio = "a changed bio"; let form: SaveUserSettings = { show_nsfw: false, @@ -755,6 +767,7 @@ export async function unfollowRemotes( await Promise.all( remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)), ); + let siteRes = await getSite(api); return siteRes; } @@ -884,14 +897,17 @@ export async function deleteAllImages(api: LemmyHttp) { limit: imageFetchLimit, }); imagesRes.images; - - for (const image of imagesRes.images) { - const form: DeleteImage = { - token: image.pictrs_delete_token, - filename: image.pictrs_alias, - }; - await api.deleteImage(form); - } + Promise.all( + imagesRes.images + .map(image => { + const form: DeleteImage = { + token: image.local_image.pictrs_delete_token, + filename: image.local_image.pictrs_alias, + }; + return form; + }) + .map(form => api.deleteImage(form)), + ); } export async function unfollows() { @@ -902,6 +918,24 @@ export async function unfollows() { unfollowRemotes(delta), unfollowRemotes(epsilon), ]); + await Promise.all([ + purgeAllPosts(alpha), + purgeAllPosts(beta), + purgeAllPosts(gamma), + purgeAllPosts(delta), + purgeAllPosts(epsilon), + ]); +} + +export async function purgeAllPosts(api: LemmyHttp) { + // The best way to get all federated items, is to find the posts + let res = await api.getPosts({ type_: "All", limit: 50 }); + await Promise.all( + Array.from(new Set(res.posts.map(p => p.post.id))) + .map(post_id => api.purgePost({ post_id })) + // Ignore errors + .map(p => p.catch(e => e)), + ); } export function getCommentParentId(comment: Comment): number | undefined { diff --git a/config/defaults.hjson b/config/defaults.hjson index ce440ab69..de13303ef 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -47,9 +47,10 @@ # # To be removed in 0.20 cache_external_link_previews: true - # Specifies how to handle remote images, so that users don't have to connect directly to remote servers. + # Specifies how to handle remote images, so that users don't have to connect directly to remote + # servers. image_mode: - # Leave images unchanged, don't generate any local thumbnails for post urls. Instead the the + # Leave images unchanged, don't generate any local thumbnails for post urls. Instead the # Opengraph image is directly returned as thumbnail "None" @@ -64,10 +65,11 @@ # or - # If enabled, all images from remote domains are rewritten to pass through `/api/v3/image_proxy`, - # including embedded images in markdown. Images are stored temporarily in pict-rs for caching. - # This improves privacy as users don't expose their IP to untrusted servers, and decreases load - # on other servers. However it increases bandwidth use for the local server. + # If enabled, all images from remote domains are rewritten to pass through + # `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily + # in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted + # servers, and decreases load on other servers. However it increases bandwidth use for the + # local server. # # Requires pict-rs 0.5 "ProxyAllImages" diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index fcbea1e5d..846583c37 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -33,7 +33,7 @@ anyhow = { workspace = true } tracing = { workspace = true } chrono = { workspace = true } url = { workspace = true } -wav = "1.0.0" +wav = "1.0.1" sitemap-rs = "0.2.1" totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] } actix-web-httpauth = "0.8.1" diff --git a/crates/api/src/comment/distinguish.rs b/crates/api/src/comment/distinguish.rs index dc2be8ad8..dfd850e89 100644 --- a/crates/api/src/comment/distinguish.rs +++ b/crates/api/src/comment/distinguish.rs @@ -17,7 +17,9 @@ 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?; + let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, @@ -54,7 +56,8 @@ pub async fn distinguish_comment( data.comment_id, Some(local_user_view.person.id), ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; Ok(Json(CommentResponse { comment_view, diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index bfa522739..d0aa4a6c2 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -35,7 +35,9 @@ 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?; + let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, @@ -46,9 +48,10 @@ pub async fn like_comment( // Add parent poster or commenter to recipients let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await; - if let Ok(reply) = comment_reply { + if let Ok(Some(reply)) = comment_reply { let recipient_id = reply.recipient_id; - if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await + if let Ok(Some(local_recipient)) = + LocalUserView::read_person(&mut context.pool(), recipient_id).await { recipient_ids.push(local_recipient.local_user.id); } diff --git a/crates/api/src/comment/list_comment_likes.rs b/crates/api/src/comment/list_comment_likes.rs index 649ae249d..8c2c9dd32 100644 --- a/crates/api/src/comment/list_comment_likes.rs +++ b/crates/api/src/comment/list_comment_likes.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ utils::is_mod_or_admin, }; use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; /// Lists likes for a comment #[tracing::instrument(skip(context))] @@ -19,7 +19,9 @@ pub async fn list_comment_likes( data.comment_id, Some(local_user_view.person.id), ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; + is_mod_or_admin( &mut context.pool(), &local_user_view.person, diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs index f197ecb4c..f9d649e48 100644 --- a/crates/api/src/comment/save.rs +++ b/crates/api/src/comment/save.rs @@ -33,7 +33,9 @@ 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?; + let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)) + .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 de40571fe..c008d1df2 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/comment_report/create.rs @@ -35,7 +35,9 @@ 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?; + let comment_view = CommentView::read(&mut context.pool(), comment_id, None) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, @@ -58,8 +60,9 @@ pub async fn create_comment_report( .await .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; - let comment_report_view = - CommentReportView::read(&mut context.pool(), report.id, person_id).await?; + let comment_report_view = CommentReportView::read(&mut context.pool(), report.id, person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommentReport)?; // Email the admins if local_site.reports_email_admins { diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/comment_report/resolve.rs index a663fdf74..40aad9569 100644 --- a/crates/api/src/comment_report/resolve.rs +++ b/crates/api/src/comment_report/resolve.rs @@ -17,7 +17,9 @@ pub async fn resolve_comment_report( ) -> LemmyResult> { let report_id = data.report_id; let person_id = local_user_view.person.id; - let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?; + let report = CommentReportView::read(&mut context.pool(), report_id, person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommentReport)?; let person_id = local_user_view.person.id; check_community_mod_action( @@ -39,8 +41,9 @@ pub async fn resolve_comment_report( } let report_id = data.report_id; - let comment_report_view = - CommentReportView::read(&mut context.pool(), report_id, person_id).await?; + let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommentReport)?; Ok(Json(CommentReportResponse { comment_report_view, diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 92799fb6b..5245f592e 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -33,9 +33,23 @@ pub async fn add_mod_to_community( &mut context.pool(), ) .await?; - let community = Community::read(&mut context.pool(), community_id).await?; + let community = Community::read(&mut context.pool(), community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; + + // If user is admin and community is remote, explicitly check that he is a + // moderator. This is necessary because otherwise the action would be rejected + // by the community's home instance. if local_user_view.local_user.admin && !community.local { - Err(LemmyErrorType::NotAModerator)? + let is_mod = CommunityModeratorView::is_community_moderator( + &mut context.pool(), + community.id, + local_user_view.person.id, + ) + .await?; + if !is_mod { + Err(LemmyErrorType::NotAModerator)? + } } // Update in local database diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 0ad9952df..93cf00415 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -89,7 +89,9 @@ pub async fn ban_from_community( ModBanFromCommunity::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), data.person_id).await?; + let person_view = PersonView::read(&mut context.pool(), data.person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; ActivityChannel::submit_activity( SendActivityData::BanFromCommunity { diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs index d5d8a0287..449addf32 100644 --- a/crates/api/src/community/block.rs +++ b/crates/api/src/community/block.rs @@ -51,7 +51,9 @@ pub async fn block_community( } let community_view = - CommunityView::read(&mut context.pool(), community_id, Some(person_id), false).await?; + CommunityView::read(&mut context.pool(), community_id, Some(person_id), 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 94109192b..853cfde14 100644 --- a/crates/api/src/community/follow.rs +++ b/crates/api/src/community/follow.rs @@ -23,7 +23,9 @@ pub async fn follow_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let community = Community::read(&mut context.pool(), data.community_id).await?; + let community = Community::read(&mut context.pool(), data.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let mut community_follower_form = CommunityFollowerForm { community_id: community.id, person_id: local_user_view.person.id, @@ -62,7 +64,10 @@ 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?; + CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; Ok(Json(CommunityResponse { diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index 399eeadae..5f3a6032e 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -79,8 +79,8 @@ pub async fn transfer_community( let person_id = local_user_view.person.id; let community_view = CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; + .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/lib.rs b/crates/api/src/lib.rs index 498dff3bd..c20e4ff9c 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -44,6 +44,7 @@ pub mod site; pub mod sitemap; /// Converts the captcha to a base64 encoded wav audio file +#[allow(deprecated)] pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult { let letters = captcha.as_wav(); @@ -252,7 +253,9 @@ pub async fn local_user_view_from_jwt( let local_user_id = Claims::validate(jwt, context) .await .with_lemmy_type(LemmyErrorType::NotLoggedIn)?; - let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; + let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id) + .await? + .ok_or(LemmyErrorType::CouldntFindLocalUser)?; check_user_valid(&local_user_view.person)?; Ok(local_user_view) diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs index 6ebdbd08f..7db2e5653 100644 --- a/crates/api/src/local_user/add_admin.rs +++ b/crates/api/src/local_user/add_admin.rs @@ -26,10 +26,10 @@ pub async fn add_admin( // Make sure that the person_id added is local let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id) - .await - .with_lemmy_type(LemmyErrorType::ObjectNotLocal)?; + .await? + .ok_or(LemmyErrorType::ObjectNotLocal)?; - let added_admin = LocalUser::update( + LocalUser::update( &mut context.pool(), added_local_user.local_user.id, &LocalUserUpdateForm { @@ -43,7 +43,7 @@ pub async fn add_admin( // Mod tables let form = ModAddForm { mod_person_id: local_user_view.person.id, - other_person_id: added_admin.person_id, + other_person_id: added_local_user.person.id, removed: Some(!data.added), }; diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index f0e6ffc26..c31940fba 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -49,7 +49,7 @@ pub async fn ban_from_site( // if its a local user, invalidate logins let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await; - if let Ok(local_user) = local_user { + if let Ok(Some(local_user)) = local_user { LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?; } @@ -70,7 +70,9 @@ pub async fn ban_from_site( ModBan::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), person.id).await?; + let person_view = PersonView::read(&mut context.pool(), person.id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; ban_nonlocal_user_from_local_communities( &local_user_view, diff --git a/crates/api/src/local_user/block.rs b/crates/api/src/local_user/block.rs index 46a1e44aa..698703a9b 100644 --- a/crates/api/src/local_user/block.rs +++ b/crates/api/src/local_user/block.rs @@ -30,8 +30,12 @@ pub async fn block_person( target_id, }; - let target_user = LocalUserView::read_person(&mut context.pool(), target_id).await; - if target_user.map(|t| t.local_user.admin) == Ok(true) { + let target_user = LocalUserView::read_person(&mut context.pool(), target_id) + .await + .ok() + .flatten(); + + if target_user.is_some_and(|t| t.local_user.admin) { Err(LemmyErrorType::CantBlockAdmin)? } @@ -45,7 +49,9 @@ pub async fn block_person( .with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?; } - let person_view = PersonView::read(&mut context.pool(), target_id).await?; + let person_view = PersonView::read(&mut context.pool(), target_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; Ok(Json(BlockPersonResponse { person_view, blocked: data.block, diff --git a/crates/api/src/local_user/change_password_after_reset.rs b/crates/api/src/local_user/change_password_after_reset.rs index 9d759d7e9..f86421796 100644 --- a/crates/api/src/local_user/change_password_after_reset.rs +++ b/crates/api/src/local_user/change_password_after_reset.rs @@ -19,9 +19,10 @@ pub async fn change_password_after_reset( ) -> LemmyResult> { // Fetch the user_id from the token let token = data.token.clone(); - let local_user_id = PasswordResetRequest::read_from_token(&mut context.pool(), &token) - .await - .map(|p| p.local_user_id)?; + let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token) + .await? + .ok_or(LemmyErrorType::TokenNotFound)? + .local_user_id; password_length_check(&data.password)?; diff --git a/crates/api/src/local_user/generate_totp_secret.rs b/crates/api/src/local_user/generate_totp_secret.rs index 285fa2ad5..f2bfe7f9c 100644 --- a/crates/api/src/local_user/generate_totp_secret.rs +++ b/crates/api/src/local_user/generate_totp_secret.rs @@ -1,11 +1,7 @@ use crate::{build_totp_2fa, generate_totp_2fa_secret}; use activitypub_federation::config::Data; use actix_web::web::Json; -use lemmy_api_common::{ - context::LemmyContext, - person::GenerateTotpSecretResponse, - sensitive::Sensitive, -}; +use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse}; use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm}; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -17,7 +13,9 @@ pub async fn generate_totp_secret( local_user_view: LocalUserView, context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; if local_user_view.local_user.totp_2fa_enabled { return Err(LemmyErrorType::TotpAlreadyEnabled)?; @@ -39,6 +37,6 @@ pub async fn generate_totp_secret( .await?; Ok(Json(GenerateTotpSecretResponse { - totp_secret_url: Sensitive::new(secret_url), + totp_secret_url: secret_url.into(), })) } diff --git a/crates/api/src/local_user/list_media.rs b/crates/api/src/local_user/list_media.rs index bf69c4a34..779558dab 100644 --- a/crates/api/src/local_user/list_media.rs +++ b/crates/api/src/local_user/list_media.rs @@ -3,8 +3,7 @@ use lemmy_api_common::{ context::LemmyContext, person::{ListMedia, ListMediaResponse}, }; -use lemmy_db_schema::source::images::LocalImage; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalImageView, LocalUserView}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -15,7 +14,7 @@ pub async fn list_media( ) -> LemmyResult> { let page = data.page; let limit = data.limit; - let images = LocalImage::get_all_paged_by_local_user_id( + let images = LocalImageView::get_all_paged_by_local_user_id( &mut context.pool(), local_user_view.local_user.id, page, diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index 8e3301af9..19f84f703 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -16,7 +16,7 @@ use lemmy_db_schema::{ RegistrationMode, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] pub async fn login( @@ -24,14 +24,16 @@ pub async fn login( req: HttpRequest, context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; // Fetch that username / email let username_or_email = data.username_or_email.clone(); let local_user_view = LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email) - .await - .with_lemmy_type(LemmyErrorType::IncorrectLogin)?; + .await? + .ok_or(LemmyErrorType::IncorrectLogin)?; // Verify the password let valid: bool = verify( @@ -79,7 +81,9 @@ async fn check_registration_application( // Fetch the registration application. If no admin id is present its still pending. Otherwise it // was processed (either accepted or denied). let local_user_id = local_user_view.local_user.id; - let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?; + let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id) + .await? + .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; if registration.admin_id.is_some() { Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? } else { diff --git a/crates/api/src/local_user/notifications/mark_mention_read.rs b/crates/api/src/local_user/notifications/mark_mention_read.rs index 9a839b2b4..90c8efb6e 100644 --- a/crates/api/src/local_user/notifications/mark_mention_read.rs +++ b/crates/api/src/local_user/notifications/mark_mention_read.rs @@ -18,7 +18,9 @@ pub async fn mark_person_mention_as_read( local_user_view: LocalUserView, ) -> LemmyResult> { let person_mention_id = data.person_mention_id; - let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?; + let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPersonMention)?; if local_user_view.person.id != read_person_mention.recipient_id { Err(LemmyErrorType::CouldntUpdateComment)? @@ -37,7 +39,9 @@ pub async fn mark_person_mention_as_read( let person_mention_id = read_person_mention.id; let person_id = local_user_view.person.id; let person_mention_view = - PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?; + PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)) + .await? + .ok_or(LemmyErrorType::CouldntFindPersonMention)?; Ok(Json(PersonMentionResponse { person_mention_view, diff --git a/crates/api/src/local_user/notifications/mark_reply_read.rs b/crates/api/src/local_user/notifications/mark_reply_read.rs index 5b263145f..fdcfa5727 100644 --- a/crates/api/src/local_user/notifications/mark_reply_read.rs +++ b/crates/api/src/local_user/notifications/mark_reply_read.rs @@ -18,7 +18,9 @@ pub async fn mark_reply_as_read( local_user_view: LocalUserView, ) -> LemmyResult> { let comment_reply_id = data.comment_reply_id; - let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?; + let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommentReply)?; if local_user_view.person.id != read_comment_reply.recipient_id { Err(LemmyErrorType::CouldntUpdateComment)? @@ -38,7 +40,9 @@ pub async fn mark_reply_as_read( let comment_reply_id = read_comment_reply.id; let person_id = local_user_view.person.id; let comment_reply_view = - CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?; + CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)) + .await? + .ok_or(LemmyErrorType::CouldntFindCommentReply)?; Ok(Json(CommentReplyResponse { comment_reply_view })) } diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs index 414f506ba..b6b113c07 100644 --- a/crates/api/src/local_user/reset_password.rs +++ b/crates/api/src/local_user/reset_password.rs @@ -6,9 +6,8 @@ use lemmy_api_common::{ utils::send_password_reset_email, SuccessResponse, }; -use lemmy_db_schema::source::password_reset_request::PasswordResetRequest; use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] pub async fn reset_password( @@ -18,19 +17,12 @@ pub async fn reset_password( // Fetch that email let email = data.email.to_lowercase(); let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email) - .await - .with_lemmy_type(LemmyErrorType::IncorrectLogin)?; + .await? + .ok_or(LemmyErrorType::IncorrectLogin)?; - // Check for too many attempts (to limit potential abuse) - let recent_resets_count = PasswordResetRequest::get_recent_password_resets_count( - &mut context.pool(), - local_user_view.local_user.id, - ) - .await?; - if recent_resets_count >= 3 { - Err(LemmyErrorType::PasswordResetLimitReached)? - } - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; check_email_verified(&local_user_view, &site_view)?; // Email the pure token to the user. diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index 3fa835f58..bdba817cc 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -28,6 +28,7 @@ use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id}, }; +use std::ops::Deref; #[tracing::instrument(skip(context))] pub async fn save_user_settings( @@ -35,7 +36,9 @@ pub async fn save_user_settings( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let slur_regex = local_site_to_slur_regex(&site_view.local_site); let url_blocklist = get_url_blocklist(&context).await?; @@ -55,7 +58,7 @@ pub async fn save_user_settings( if let Some(Some(email)) = &email { let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); // if email was changed, check that it is not taken and send verification mail - if &previous_email != email { + if previous_email.deref() != email { if LocalUser::is_email_taken(&mut context.pool(), email).await? { return Err(LemmyErrorType::EmailAlreadyExists)?; } @@ -69,7 +72,8 @@ pub async fn save_user_settings( } } - // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value + // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None + // value if let Some(email) = &email { if email.is_none() && site_view.local_site.require_email_verification { Err(LemmyErrorType::EmailRequired)? @@ -139,11 +143,7 @@ pub async fn save_user_settings( ..Default::default() }; - // Ignore errors, because 'no fields updated' will return an error. - // https://github.com/LemmyNet/lemmy/issues/4076 - LocalUser::update(&mut context.pool(), local_user_id, &local_user_form) - .await - .ok(); + LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?; // Update the vote display modes let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm { diff --git a/crates/api/src/local_user/verify_email.rs b/crates/api/src/local_user/verify_email.rs index 94ddb373a..5f38ffc12 100644 --- a/crates/api/src/local_user/verify_email.rs +++ b/crates/api/src/local_user/verify_email.rs @@ -9,23 +9,23 @@ use lemmy_db_schema::{ source::{ email_verification::EmailVerification, local_user::{LocalUser, LocalUserUpdateForm}, - person::Person, }, - traits::Crud, RegistrationMode, }; -use lemmy_db_views::structs::SiteView; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; pub async fn verify_email( data: Json, context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let token = data.token.clone(); let verification = EmailVerification::read_for_token(&mut context.pool(), &token) - .await - .with_lemmy_type(LemmyErrorType::TokenNotFound)?; + .await? + .ok_or(LemmyErrorType::TokenNotFound)?; let form = LocalUserUpdateForm { // necessary in case this is a new signup @@ -36,7 +36,7 @@ pub async fn verify_email( }; let local_user_id = verification.local_user_id; - let local_user = LocalUser::update(&mut context.pool(), local_user_id, &form).await?; + LocalUser::update(&mut context.pool(), local_user_id, &form).await?; EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?; @@ -44,9 +44,16 @@ pub async fn verify_email( if site_view.local_site.registration_mode == RegistrationMode::RequireApplication && site_view.local_site.application_email_admins { - let person = Person::read(&mut context.pool(), local_user.person_id).await?; - send_new_applicant_email_to_admins(&person.name, &mut context.pool(), context.settings()) - .await?; + let local_user = LocalUserView::read(&mut context.pool(), local_user_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; + + send_new_applicant_email_to_admins( + &local_user.person.name, + &mut context.pool(), + context.settings(), + ) + .await?; } Ok(Json(SuccessResponse::default())) diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index 566ca3a0b..40cbf6794 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -16,7 +16,7 @@ use lemmy_db_schema::{ PostFeatureType, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn feature_post( @@ -25,7 +25,9 @@ pub async fn feature_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; check_community_mod_action( &local_user_view.person, diff --git a/crates/api/src/post/get_link_metadata.rs b/crates/api/src/post/get_link_metadata.rs index 8bc425125..17346790a 100644 --- a/crates/api/src/post/get_link_metadata.rs +++ b/crates/api/src/post/get_link_metadata.rs @@ -11,7 +11,7 @@ pub async fn get_link_metadata( data: Query, context: Data, ) -> LemmyResult> { - let metadata = fetch_link_metadata(&data.url, false, &context).await?; + let metadata = fetch_link_metadata(&data.url, &context).await?; Ok(Json(GetSiteMetadataResponse { metadata })) } diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index 1cbdba8e2..fccd9f8df 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -38,7 +38,9 @@ pub async fn like_post( // Check for a community ban let post_id = data.post_id; - let post = Post::read(&mut context.pool(), post_id).await?; + let post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; check_community_user_action( &local_user_view.person, @@ -66,14 +68,17 @@ pub async fn like_post( .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; } - // Mark the post as read mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + let community = Community::read(&mut context.pool(), post.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; + ActivityChannel::submit_activity( SendActivityData::LikePostOrComment { object_id: post.ap_id, actor: local_user_view.person.clone(), - community: Community::read(&mut context.pool(), post.community_id).await?, + community, score: data.score, }, &context, diff --git a/crates/api/src/post/list_post_likes.rs b/crates/api/src/post/list_post_likes.rs index a9b302f2e..b9b2106b7 100644 --- a/crates/api/src/post/list_post_likes.rs +++ b/crates/api/src/post/list_post_likes.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{source::post::Post, traits::Crud}; use lemmy_db_views::structs::{LocalUserView, VoteView}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; /// Lists likes for a post #[tracing::instrument(skip(context))] @@ -15,7 +15,9 @@ pub async fn list_post_likes( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let post = Post::read(&mut context.pool(), data.post_id).await?; + let post = Post::read(&mut context.pool(), data.post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; is_mod_or_admin( &mut context.pool(), &local_user_view.person, diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index b48c415af..05db8ebbb 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -15,7 +15,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn lock_post( @@ -24,7 +24,9 @@ pub async fn lock_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; check_community_mod_action( &local_user_view.person, diff --git a/crates/api/src/post/save.rs b/crates/api/src/post/save.rs index 05c7b3589..96dd85579 100644 --- a/crates/api/src/post/save.rs +++ b/crates/api/src/post/save.rs @@ -34,9 +34,10 @@ 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?; + let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; - // Mark the post as read mark_post_as_read(person_id, post_id, &mut context.pool()).await?; Ok(Json(PostResponse { post_view })) diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs index 590c9af40..72a40f70d 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/post_report/create.rs @@ -35,7 +35,9 @@ pub async fn create_post_report( let person_id = local_user_view.person.id; let post_id = data.post_id; - let post_view = PostView::read(&mut context.pool(), post_id, None, false).await?; + let post_view = PostView::read(&mut context.pool(), post_id, None, false) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; check_community_user_action( &local_user_view.person, @@ -59,7 +61,9 @@ pub async fn create_post_report( .await .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; - let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?; + let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPostReport)?; // Email the admins if local_site.reports_email_admins { diff --git a/crates/api/src/post_report/resolve.rs b/crates/api/src/post_report/resolve.rs index a3cb85c6c..428619674 100644 --- a/crates/api/src/post_report/resolve.rs +++ b/crates/api/src/post_report/resolve.rs @@ -17,7 +17,9 @@ pub async fn resolve_post_report( ) -> LemmyResult> { let report_id = data.report_id; let person_id = local_user_view.person.id; - let report = PostReportView::read(&mut context.pool(), report_id, person_id).await?; + let report = PostReportView::read(&mut context.pool(), report_id, person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPostReport)?; let person_id = local_user_view.person.id; check_community_mod_action( @@ -38,7 +40,9 @@ pub async fn resolve_post_report( .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; } - let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id).await?; + let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPostReport)?; Ok(Json(PostReportResponse { post_report_view })) } diff --git a/crates/api/src/private_message/mark_read.rs b/crates/api/src/private_message/mark_read.rs index 7c213464b..07e06fe21 100644 --- a/crates/api/src/private_message/mark_read.rs +++ b/crates/api/src/private_message/mark_read.rs @@ -18,7 +18,9 @@ pub async fn mark_pm_as_read( ) -> LemmyResult> { // Checking permissions let private_message_id = data.private_message_id; - let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; + let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; if local_user_view.person.id != orig_private_message.recipient_id { Err(LemmyErrorType::CouldntUpdatePrivateMessage)? } @@ -37,7 +39,9 @@ pub async fn mark_pm_as_read( .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; - let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; + let view = PrivateMessageView::read(&mut context.pool(), private_message_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; Ok(Json(PrivateMessageResponse { private_message_view: view, })) diff --git a/crates/api/src/private_message_report/create.rs b/crates/api/src/private_message_report/create.rs index de8ca390f..41ac592ae 100644 --- a/crates/api/src/private_message_report/create.rs +++ b/crates/api/src/private_message_report/create.rs @@ -29,7 +29,9 @@ pub async fn create_pm_report( let person_id = local_user_view.person.id; let private_message_id = data.private_message_id; - let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; + let private_message = PrivateMessage::read(&mut context.pool(), private_message_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; // Make sure that only the recipient of the private message can create a report if person_id != private_message.recipient_id { @@ -47,8 +49,9 @@ pub async fn create_pm_report( .await .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; - let private_message_report_view = - PrivateMessageReportView::read(&mut context.pool(), report.id).await?; + let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report.id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?; // Email the admins if local_site.reports_email_admins { diff --git a/crates/api/src/private_message_report/resolve.rs b/crates/api/src/private_message_report/resolve.rs index 7d821a60c..27847eeaf 100644 --- a/crates/api/src/private_message_report/resolve.rs +++ b/crates/api/src/private_message_report/resolve.rs @@ -28,8 +28,9 @@ pub async fn resolve_pm_report( .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; } - let private_message_report_view = - PrivateMessageReportView::read(&mut context.pool(), report_id).await?; + let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?; Ok(Json(PrivateMessageReportResponse { private_message_report_view, diff --git a/crates/api/src/site/federated_instances.rs b/crates/api/src/site/federated_instances.rs index 5943cfd9a..66b0ff2c1 100644 --- a/crates/api/src/site/federated_instances.rs +++ b/crates/api/src/site/federated_instances.rs @@ -5,13 +5,15 @@ use lemmy_api_common::{ utils::build_federated_instances, }; use lemmy_db_views::structs::SiteView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn get_federated_instances( context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let federated_instances = build_federated_instances(&site_view.local_site, &mut context.pool()).await?; diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 52b8a32ef..e7a5464f3 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -55,7 +55,9 @@ pub async fn leave_admin( ModAdd::create(&mut context.pool(), &form).await?; // Reread site and admins - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let admins = PersonView::admins(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?; diff --git a/crates/api/src/site/list_all_media.rs b/crates/api/src/site/list_all_media.rs index 49132cd64..4d8d2dc2a 100644 --- a/crates/api/src/site/list_all_media.rs +++ b/crates/api/src/site/list_all_media.rs @@ -4,8 +4,7 @@ use lemmy_api_common::{ person::{ListMedia, ListMediaResponse}, utils::is_admin, }; -use lemmy_db_schema::source::images::LocalImage; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalImageView, LocalUserView}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -19,6 +18,6 @@ pub async fn list_all_media( let page = data.page; let limit = data.limit; - let images = LocalImage::get_all(&mut context.pool(), page, limit).await?; + let images = LocalImageView::get_all(&mut context.pool(), page, limit).await?; Ok(Json(ListMediaResponse { images })) } diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index cbb3637f0..70d95e160 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -15,7 +15,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::{CommentView, LocalUserView}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn purge_comment( @@ -29,7 +29,9 @@ 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?; + let comment_view = CommentView::read(&mut context.pool(), comment_id, None) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; 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 96c9c19cd..14b250681 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -16,7 +16,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn purge_community( @@ -28,7 +28,9 @@ pub async fn purge_community( is_admin(&local_user_view)?; // Read the community to get its images - let community = Community::read(&mut context.pool(), data.community_id).await?; + let community = Community::read(&mut context.pool(), data.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; 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 a8233f76d..1b38752c7 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -16,7 +16,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn purge_person( @@ -27,7 +27,9 @@ pub async fn purge_person( // Only let admin purge an item is_admin(&local_user_view)?; - let person = Person::read(&mut context.pool(), 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 ff34c471a..75cd021d1 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -16,7 +16,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn purge_post( @@ -28,7 +28,9 @@ pub async fn purge_post( is_admin(&local_user_view)?; // Read the post to get the community_id - let post = Post::read(&mut context.pool(), data.post_id).await?; + let post = Post::read(&mut context.pool(), data.post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; // Purge image if let Some(url) = &post.url { diff --git a/crates/api/src/site/registration_applications/approve.rs b/crates/api/src/site/registration_applications/approve.rs index df3c3b428..0fb55ffc8 100644 --- a/crates/api/src/site/registration_applications/approve.rs +++ b/crates/api/src/site/registration_applications/approve.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ utils::diesel_option_overwrite, }; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; pub async fn approve_registration_application( data: Json, @@ -45,8 +45,9 @@ pub async fn approve_registration_application( LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?; if data.approve { - let approved_local_user_view = - LocalUserView::read(&mut context.pool(), approved_user_id).await?; + 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() { send_application_approved_email(&approved_local_user_view, context.settings()).await?; @@ -54,8 +55,9 @@ pub async fn approve_registration_application( } // Read the view - let registration_application = - RegistrationApplicationView::read(&mut context.pool(), app_id).await?; + let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id) + .await? + .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; Ok(Json(RegistrationApplicationResponse { registration_application, diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 47545446f..25bc02893 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -25,7 +25,7 @@ full = [ "lemmy_db_views_moderator/full", "lemmy_utils/full", "activitypub_federation", - "encoding", + "encoding_rs", "reqwest-middleware", "webpage", "ts-rs", @@ -66,13 +66,13 @@ actix-web = { workspace = true, optional = true } enum-map = { workspace = true } urlencoding = { workspace = true } mime = { version = "0.3.17", optional = true } -webpage = { version = "1.6", default-features = false, features = [ +webpage = { version = "2.0", default-features = false, features = [ "serde", ], optional = true } -encoding = { version = "0.2.33", optional = true } -jsonwebtoken = { version = "8.3.0", optional = true } +encoding_rs = { version = "0.8.34", optional = true } +jsonwebtoken = { version = "9.3.0", optional = true } # necessary for wasmt compilation -getrandom = { version = "0.2.12", features = ["js"] } +getrandom = { version = "0.2.15", features = ["js"] } [package.metadata.cargo-machete] ignored = ["getrandom"] diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs index cd55d399c..85cf065eb 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -27,6 +27,7 @@ use lemmy_db_views_actor::structs::CommunityView; use lemmy_utils::{ error::LemmyResult, utils::{markdown::markdown_to_html, mention::MentionData}, + LemmyErrorType, }; pub async fn build_comment_response( @@ -36,7 +37,9 @@ pub async fn build_comment_response( 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).await?; + let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; Ok(CommentResponse { comment_view, recipient_ids, @@ -58,7 +61,8 @@ pub async fn build_community_response( Some(person_id), is_mod_or_admin, ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; Ok(Json(CommunityResponse { @@ -82,7 +86,8 @@ pub async fn build_post_response( Some(person.id), is_mod_or_admin, ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; Ok(Json(PostResponse { post_view })) } @@ -99,7 +104,9 @@ pub async fn send_local_notifs( let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); // Read the comment view to get extra info - let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?; + let comment_view = CommentView::read(&mut context.pool(), comment_id, None) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; let comment = comment_view.comment; let post = comment_view.post; let community = comment_view.community; @@ -111,10 +118,11 @@ pub async fn send_local_notifs( { let mention_name = mention.name.clone(); let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await; - if let Ok(mention_user_view) = user_view { + if let Ok(Some(mention_user_view)) = user_view { // TODO // At some point, make it so you can't tag the parent creator either - // Potential duplication of notifications, one for reply and the other for mention, is handled below by checking recipient ids + // Potential duplication of notifications, one for reply and the other for mention, is handled + // below by checking recipient ids recipient_ids.push(mention_user_view.local_user.id); let user_mention_form = PersonMentionInsertForm { @@ -146,7 +154,9 @@ pub async fn send_local_notifs( // Send comment_reply to the parent commenter / poster if let Some(parent_comment_id) = comment.parent_comment_id() { - let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; + let parent_comment = Comment::read(&mut context.pool(), parent_comment_id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; // Get the parent commenter local_user let parent_creator_id = parent_comment.creator_id; @@ -165,7 +175,7 @@ pub async fn send_local_notifs( // Don't send a notif to yourself if parent_comment.creator_id != person.id && !check_blocks { let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; - if let Ok(parent_user_view) = user_view { + if let Ok(Some(parent_user_view)) = user_view { // Don't duplicate notif if already mentioned by checking recipient ids if !recipient_ids.contains(&parent_user_view.local_user.id) { recipient_ids.push(parent_user_view.local_user.id); @@ -212,7 +222,7 @@ pub async fn send_local_notifs( if post.creator_id != person.id && !check_blocks { let creator_id = post.creator_id; let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; - if let Ok(parent_user_view) = parent_user { + if let Ok(Some(parent_user_view)) = parent_user { if !recipient_ids.contains(&parent_user_view.local_user.id) { recipient_ids.push(parent_user_view.local_user.id); diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 19145488a..160c581cd 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -1,9 +1,10 @@ -use crate::{context::LemmyContext, sensitive::Sensitive}; +use crate::context::LemmyContext; use actix_web::{http::header::USER_AGENT, HttpRequest}; use chrono::Utc; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use lemmy_db_schema::{ newtypes::LocalUserId, + sensitive::SensitiveString, source::login_token::{LoginToken, LoginTokenCreateForm}, }; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; @@ -40,7 +41,7 @@ impl Claims { user_id: LocalUserId, req: HttpRequest, context: &LemmyContext, - ) -> LemmyResult> { + ) -> LemmyResult { let hostname = context.settings().hostname.clone(); let my_claims = Claims { sub: user_id.0.to_string(), @@ -50,7 +51,7 @@ impl Claims { let secret = &context.secret().jwt_secret; let key = EncodingKey::from_secret(secret.as_ref()); - let token = encode(&Header::default(), &my_claims, &key)?; + let token: SensitiveString = encode(&Header::default(), &my_claims, &key)?.into(); let ip = req .connection_info() .realip_remote_addr() @@ -67,7 +68,7 @@ impl Claims { user_agent, }; LoginToken::create(&mut context.pool(), form).await?; - Ok(Sensitive::new(token)) + Ok(token) } } @@ -99,7 +100,7 @@ mod tests { async fn test_should_not_validate_user_token_after_password_change() { let pool_ = build_db_pool_for_tests().await; let pool = &mut (&pool_).into(); - let secret = Secret::init(pool).await.unwrap(); + let secret = Secret::init(pool).await.unwrap().unwrap(); let context = LemmyContext::create( pool_.clone(), ClientBuilder::new(Client::default()).build(), diff --git a/crates/api_common/src/context.rs b/crates/api_common/src/context.rs index ba9eb4074..f4ac41db1 100644 --- a/crates/api_common/src/context.rs +++ b/crates/api_common/src/context.rs @@ -64,7 +64,7 @@ impl LemmyContext { let client = ClientBuilder::new(client).build(); let secret = Secret { id: 0, - jwt_secret: String::new(), + jwt_secret: String::new().into(), }; let rate_limit_cell = RateLimitCell::with_test_config(); diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 4bb7ada5b..9d12d2e13 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -14,7 +14,6 @@ pub mod private_message; pub mod request; #[cfg(feature = "full")] pub mod send_activity; -pub mod sensitive; pub mod site; #[cfg(feature = "full")] pub mod utils; diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index a4f9b64d9..f61f784c2 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -1,13 +1,13 @@ -use crate::sensitive::Sensitive; use lemmy_db_schema::{ newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, - source::{images::LocalImage, site::Site}, + sensitive::SensitiveString, + source::site::Site, CommentSortType, ListingType, PostListingMode, SortType, }; -use lemmy_db_views::structs::{CommentView, PostView}; +use lemmy_db_views::structs::{CommentView, LocalImageView, PostView}; use lemmy_db_views_actor::structs::{ CommentReplyView, CommunityModeratorView, @@ -25,8 +25,8 @@ use ts_rs::TS; #[cfg_attr(feature = "full", ts(export))] /// Logging into lemmy. pub struct Login { - pub username_or_email: Sensitive, - pub password: Sensitive, + pub username_or_email: SensitiveString, + pub password: SensitiveString, /// May be required, if totp is enabled for their account. pub totp_2fa_token: Option, } @@ -38,11 +38,11 @@ pub struct Login { /// Register / Sign up to lemmy. pub struct Register { pub username: String, - pub password: Sensitive, - pub password_verify: Sensitive, - pub show_nsfw: bool, + pub password: SensitiveString, + pub password_verify: SensitiveString, + pub show_nsfw: Option, /// email is mandatory if email verification is enabled on the server - pub email: Option>, + pub email: Option, /// The UUID of the captcha item. pub captcha_uuid: Option, /// Your captcha answer. @@ -99,7 +99,7 @@ pub struct SaveUserSettings { /// Your display name, which can contain strange characters, and does not need to be unique. pub display_name: Option, /// Your email. - pub email: Option>, + pub email: Option, /// Your bio / info, in markdown. pub bio: Option, /// Your matrix user id. Ex: @my_user:matrix.org @@ -124,7 +124,8 @@ pub struct SaveUserSettings { pub post_listing_mode: Option, /// Whether to allow keyboard navigation (for browsing and interacting with posts and comments). pub enable_keyboard_navigation: Option, - /// Whether user avatars or inline images in the UI that are gifs should be allowed to play or should be paused + /// Whether user avatars or inline images in the UI that are gifs should be allowed to play or + /// should be paused pub enable_animated_images: Option, /// Whether to auto-collapse bot comments. pub collapse_bot_comments: Option, @@ -140,9 +141,9 @@ pub struct SaveUserSettings { #[cfg_attr(feature = "full", ts(export))] /// Changes your account password. pub struct ChangePassword { - pub new_password: Sensitive, - pub new_password_verify: Sensitive, - pub old_password: Sensitive, + pub new_password: SensitiveString, + pub new_password_verify: SensitiveString, + pub old_password: SensitiveString, } #[skip_serializing_none] @@ -151,8 +152,9 @@ pub struct ChangePassword { #[cfg_attr(feature = "full", ts(export))] /// A response for your login. pub struct LoginResponse { - /// This is None in response to `Register` if email verification is enabled, or the server requires registration applications. - pub jwt: Option>, + /// This is None in response to `Register` if email verification is enabled, or the server + /// requires registration applications. + pub jwt: Option, /// If registration applications are required, this will return true for a signup response. pub registration_created: bool, /// If email verifications are required, this will return true for a signup response. @@ -340,7 +342,7 @@ pub struct CommentReplyResponse { #[cfg_attr(feature = "full", ts(export))] /// Delete your account. pub struct DeleteAccount { - pub password: Sensitive, + pub password: SensitiveString, pub delete_content: bool, } @@ -349,7 +351,7 @@ pub struct DeleteAccount { #[cfg_attr(feature = "full", ts(export))] /// Reset your password via email. pub struct PasswordReset { - pub email: Sensitive, + pub email: SensitiveString, } #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] @@ -357,9 +359,9 @@ pub struct PasswordReset { #[cfg_attr(feature = "full", ts(export))] /// Change your password after receiving a reset request. pub struct PasswordChangeAfterReset { - pub token: Sensitive, - pub password: Sensitive, - pub password_verify: Sensitive, + pub token: SensitiveString, + pub password: SensitiveString, + pub password_verify: SensitiveString, } #[skip_serializing_none] @@ -405,7 +407,7 @@ pub struct VerifyEmail { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] pub struct GenerateTotpSecretResponse { - pub totp_secret_url: Sensitive, + pub totp_secret_url: SensitiveString, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] @@ -437,5 +439,5 @@ pub struct ListMedia { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] pub struct ListMediaResponse { - pub images: Vec, + pub images: Vec, } diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 4993fc6e5..49327dac1 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -270,8 +270,6 @@ pub struct LinkMetadata { #[serde(flatten)] pub opengraph_data: OpenGraphData, pub content_type: Option, - #[serde(skip)] - pub thumbnail: Option, } #[skip_serializing_none] diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index 912fbc67e..c304bcba7 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -3,10 +3,11 @@ use crate::{ lemmy_db_schema::traits::Crud, post::{LinkMetadata, OpenGraphData}, send_activity::{ActivityChannel, SendActivityData}, - utils::{local_site_opt_to_sensitive, proxy_image_link, proxy_image_link_opt_apub}, + utils::{local_site_opt_to_sensitive, proxy_image_link}, }; use activitypub_federation::config::Data; -use encoding::{all::encodings, DecoderTrap}; +use chrono::{DateTime, Utc}; +use encoding_rs::{Encoding, UTF_8}; use lemmy_db_schema::{ newtypes::DbUrl, source::{ @@ -18,14 +19,13 @@ use lemmy_db_schema::{ use lemmy_utils::{ error::{LemmyError, LemmyErrorType, LemmyResult}, settings::structs::{PictrsImageMode, Settings}, - spawn_try_task, REQWEST_TIMEOUT, VERSION, }; use mime::Mime; use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder}; use reqwest_middleware::ClientWithMiddleware; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tracing::info; use url::Url; use urlencoding::encode; @@ -42,11 +42,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder { /// Fetches metadata for the given link and optionally generates thumbnail. #[tracing::instrument(skip_all)] -pub async fn fetch_link_metadata( - url: &Url, - generate_thumbnail: bool, - context: &LemmyContext, -) -> LemmyResult { +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?; @@ -63,118 +59,85 @@ pub async fn fetch_link_metadata( let opengraph_data = extract_opengraph_data(&html_bytes, url) .map_err(|e| info!("{e}")) .unwrap_or_default(); - let thumbnail = - extract_thumbnail_from_opengraph_data(url, &opengraph_data, generate_thumbnail, context).await; - Ok(LinkMetadata { opengraph_data, content_type: content_type.map(|c| c.to_string()), - thumbnail, }) } -#[tracing::instrument(skip_all)] -pub async fn fetch_link_metadata_opt( - url: Option<&Url>, - generate_thumbnail: bool, - context: &LemmyContext, -) -> LinkMetadata { - match &url { - Some(url) => fetch_link_metadata(url, generate_thumbnail, context) - .await - .unwrap_or_default(), - _ => Default::default(), - } -} -/// Generate post thumbnail in background task, because some sites can be very slow to respond. +/// 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. -pub fn generate_post_link_metadata( +/// +/// TODO: `federated_thumbnail` param can be removed once we federate full metadata and can +/// write it to db directly, without calling this function. +/// https://github.com/LemmyNet/lemmy/issues/4598 +pub async fn generate_post_link_metadata( post: Post, custom_thumbnail: Option, send_activity: impl FnOnce(Post) -> Option + Send + 'static, local_site: Option, context: Data, -) { - spawn_try_task(async move { - // Decide if the thumbnail should be generated - let allow_sensitive = local_site_opt_to_sensitive(&local_site); - let page_is_sensitive = post.nsfw; - let allow_generate_thumbnail = allow_sensitive || !page_is_sensitive; - let do_generate_thumbnail = - allow_generate_thumbnail && custom_thumbnail.is_none() && post.thumbnail_url.is_none(); +) -> LemmyResult<()> { + let metadata = match &post.url { + Some(url) => fetch_link_metadata(url, &context).await.unwrap_or_default(), + _ => Default::default(), + }; - // Generate local thumbnail only if no thumbnail was federated and 'sensitive' attributes allow it. - let metadata = fetch_link_metadata_opt( - post.url.as_ref().map(DbUrl::inner), - do_generate_thumbnail, - &context, - ) - .await; + let is_image_post = metadata + .content_type + .as_ref() + .is_some_and(|content_type| content_type.starts_with("image")); - // If its an image post, it needs to overwrite the thumbnail, and take precedence - let image_url = if metadata - .content_type - .as_ref() - .is_some_and(|content_type| content_type.starts_with("image")) - { - post.url.map(Into::into) - } else { - None - }; + // Decide if we are allowed to generate local thumbnail + let allow_sensitive = local_site_opt_to_sensitive(&local_site); + let allow_generate_thumbnail = allow_sensitive || !post.nsfw; - // Build the thumbnail url based on either the post image url, custom thumbnail, metadata fetch, or existing thumbnail. - let thumbnail_url = image_url - .or(custom_thumbnail) - .or(metadata.thumbnail.map(Into::into)) - .or(post.thumbnail_url.map(Into::into)); + let image_url = if is_image_post { + post.url + } else { + metadata.opengraph_data.image.clone() + }; - // Proxy the image fetch if necessary - let proxied_thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, &context).await?; + let thumbnail_url = if let (false, Some(url)) = (is_image_post, custom_thumbnail) { + proxy_image_link(url, &context).await.ok() + } else if let (true, Some(url)) = (allow_generate_thumbnail, image_url) { + generate_pictrs_thumbnail(&url, &context) + .await + .ok() + .map(Into::into) + } else { + metadata.opengraph_data.image.clone() + }; - let form = PostUpdateForm { - embed_title: Some(metadata.opengraph_data.title), - embed_description: Some(metadata.opengraph_data.description), - embed_video_url: Some(metadata.opengraph_data.embed_video_url), - thumbnail_url: Some(proxied_thumbnail_url), - url_content_type: Some(metadata.content_type), - ..Default::default() - }; - let updated_post = Post::update(&mut context.pool(), post.id, &form).await?; - if let Some(send_activity) = send_activity(updated_post) { - ActivityChannel::submit_activity(send_activity, &context).await?; - } - Ok(()) - }); + let form = PostUpdateForm { + embed_title: Some(metadata.opengraph_data.title), + embed_description: Some(metadata.opengraph_data.description), + embed_video_url: Some(metadata.opengraph_data.embed_video_url), + thumbnail_url: Some(thumbnail_url), + url_content_type: Some(metadata.content_type), + ..Default::default() + }; + let updated_post = Post::update(&mut context.pool(), post.id, &form).await?; + if let Some(send_activity) = send_activity(updated_post) { + ActivityChannel::submit_activity(send_activity, &context).await?; + } + Ok(()) } /// Extract site metadata from HTML Opengraph attributes. fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult { let html = String::from_utf8_lossy(html_bytes); - // Make sure the first line is doctype html - let first_line = html - .trim_start() - .lines() - .next() - .ok_or(LemmyErrorType::NoLinesInHtml)? - .to_lowercase(); - - if !first_line.starts_with(" LemmyResult Option { - if generate_thumbnail { - let image_url = opengraph_data - .image - .as_ref() - .map(DbUrl::inner) - .unwrap_or(url); - generate_pictrs_thumbnail(image_url, context) - .await - .ok() - .map(Into::into) - } else { - opengraph_data.image.clone() +#[derive(Deserialize, Serialize, Debug)] +pub struct PictrsResponse { + pub files: Option>, + pub msg: String, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct PictrsFile { + pub file: String, + pub delete_token: String, + pub details: PictrsFileDetails, +} + +impl PictrsFile { + pub fn thumbnail_url(&self, protocol_and_hostname: &str) -> Result { + Url::parse(&format!( + "{protocol_and_hostname}/pictrs/image/{}", + self.file + )) } } -#[derive(Deserialize, Debug)] -struct PictrsResponse { - files: Vec, - msg: String, +/// Stores extra details about a Pictrs image. +#[derive(Deserialize, Serialize, Debug)] +pub struct PictrsFileDetails { + /// In pixels + pub width: u16, + /// In pixels + pub height: u16, + pub content_type: String, + pub created_at: DateTime, } -#[derive(Deserialize, Debug)] -struct PictrsFile { - file: String, - delete_token: String, -} - -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] struct PictrsPurgeResponse { msg: String, } @@ -329,33 +291,34 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L encode(image_url.as_str()) ); - let response = context + let res = context .client() .get(&fetch_url) .timeout(REQWEST_TIMEOUT) .send() + .await? + .json::() .await?; - let response: PictrsResponse = response.json().await?; + let files = res.files.unwrap_or_default(); - if response.msg == "ok" { - let thumbnail_url = Url::parse(&format!( - "{}/pictrs/image/{}", - context.settings().get_protocol_and_hostname(), - response.files.first().expect("missing pictrs file").file - ))?; - for uploaded_image in response.files { - let form = LocalImageForm { - local_user_id: None, - pictrs_alias: uploaded_image.file.to_string(), - pictrs_delete_token: uploaded_image.delete_token.to_string(), - }; - LocalImage::create(&mut context.pool(), &form).await?; - } - Ok(thumbnail_url) - } else { - Err(LemmyErrorType::PictrsResponseError(response.msg))? - } + let image = files + .first() + .ok_or(LemmyErrorType::PictrsResponseError(res.msg))?; + + let form = LocalImageForm { + // This is none because its an internal request. + // IE, a local user shouldn't get to delete the thumbnails for their link posts + local_user_id: None, + pictrs_alias: image.file.clone(), + pictrs_delete_token: image.delete_token.clone(), + }; + 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?; + + Ok(thumbnail_url) } // TODO: get rid of this by reading content type from db @@ -414,9 +377,7 @@ mod tests { async fn test_link_metadata() { let context = LemmyContext::init_test_context().await; let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap(); - let sample_res = fetch_link_metadata(&sample_url, false, &context) - .await - .unwrap(); + let sample_res = fetch_link_metadata(&sample_url, &context).await.unwrap(); assert_eq!( Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()), sample_res.opengraph_data.title @@ -438,17 +399,8 @@ mod tests { Some(mime::TEXT_HTML_UTF_8.to_string()), sample_res.content_type ); - assert!(sample_res.thumbnail.is_some()); } - // #[test] - // fn test_pictshare() { - // let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg"); - // assert!(res.is_ok()); - // let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu"); - // assert!(res_other.is_err()); - // } - #[test] fn test_resolve_image_url() { // url that lists the opengraph fields diff --git a/crates/api_common/src/sensitive.rs b/crates/api_common/src/sensitive.rs deleted file mode 100644 index 4dd120805..000000000 --- a/crates/api_common/src/sensitive.rs +++ /dev/null @@ -1,116 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{ - borrow::Borrow, - ops::{Deref, DerefMut}, -}; -#[cfg(feature = "full")] -use ts_rs::TS; - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Default)] -#[serde(transparent)] -pub struct Sensitive(T); - -impl Sensitive { - pub fn new(item: T) -> Self { - Sensitive(item) - } - pub fn into_inner(self) -> T { - self.0 - } -} - -impl std::fmt::Debug for Sensitive { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Sensitive").finish() - } -} - -impl AsRef for Sensitive { - fn as_ref(&self) -> &T { - &self.0 - } -} - -impl AsRef for Sensitive { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl AsRef<[u8]> for Sensitive { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl AsRef<[u8]> for Sensitive> { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl AsMut for Sensitive { - fn as_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl AsMut for Sensitive { - fn as_mut(&mut self) -> &mut str { - &mut self.0 - } -} - -impl Deref for Sensitive { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Sensitive { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From for Sensitive { - fn from(t: T) -> Self { - Sensitive(t) - } -} - -impl From<&str> for Sensitive { - fn from(s: &str) -> Self { - Sensitive(s.into()) - } -} - -impl Borrow for Sensitive { - fn borrow(&self) -> &T { - &self.0 - } -} - -impl Borrow for Sensitive { - fn borrow(&self) -> &str { - &self.0 - } -} - -#[cfg(feature = "full")] -impl TS for Sensitive { - fn name() -> String { - "string".to_string() - } - fn name_with_type_args(_args: Vec) -> String { - "string".to_string() - } - fn dependencies() -> Vec { - Vec::new() - } - fn transparent() -> bool { - true - } -} diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index d87cbdaaf..77bfcd8ee 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -375,7 +375,8 @@ impl From for ReadableFederationState { pub struct InstanceWithFederationState { #[serde(flatten)] pub instance: Instance, - /// if federation to this instance is or was active, show state of outgoing federation to this instance + /// if federation to this instance is or was active, show state of outgoing federation to this + /// instance pub federation_state: Option, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 9810e2390..bad3b8180 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -6,13 +6,14 @@ use crate::{ use chrono::{DateTime, Days, Local, TimeZone, Utc}; use enum_map::{enum_map, EnumMap}; use lemmy_db_schema::{ + aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId}, source::{ comment::{Comment, CommentUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm}, community_block::CommunityBlock, email_verification::{EmailVerification, EmailVerificationForm}, - images::{LocalImage, RemoteImage}, + images::RemoteImage, instance::Instance, instance_block::InstanceBlock, local_site::LocalSite, @@ -27,7 +28,10 @@ use lemmy_db_schema::{ traits::Crud, utils::DbPool, }; -use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; +use lemmy_db_views::{ + comment_view::CommentQuery, + structs::{LocalImageView, LocalUserView}, +}; use lemmy_db_views_actor::structs::{ CommunityModeratorView, CommunityPersonBanView, @@ -136,13 +140,7 @@ pub fn is_top_mod( } } -#[tracing::instrument(skip_all)] -pub async fn get_post(post_id: PostId, pool: &mut DbPool<'_>) -> LemmyResult { - Post::read(pool, post_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindPost) -} - +/// Marks a post as read for a given person. #[tracing::instrument(skip_all)] pub async fn mark_post_as_read( person_id: PersonId, @@ -155,6 +153,28 @@ pub async fn mark_post_as_read( Ok(()) } +/// Updates the read comment count for a post. Usually done when reading or creating a new comment. +#[tracing::instrument(skip_all)] +pub async fn update_read_comments( + person_id: PersonId, + post_id: PostId, + read_comments: i64, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + let person_post_agg_form = PersonPostAggregatesForm { + person_id, + post_id, + read_comments, + ..PersonPostAggregatesForm::default() + }; + + PersonPostAggregates::upsert(pool, &person_post_agg_form) + .await + .with_lemmy_type(LemmyErrorType::CouldntFindPost)?; + + Ok(()) +} + pub fn check_user_valid(person: &Person) -> LemmyResult<()> { // Check for a site ban if person.banned { @@ -188,8 +208,8 @@ async fn check_community_deleted_removed( pool: &mut DbPool<'_>, ) -> LemmyResult<()> { let community = Community::read(pool, community_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if community.deleted || community.removed { Err(LemmyErrorType::Deleted)? } @@ -353,7 +373,8 @@ pub async fn build_federated_instances( federation_state: federation_state.map(std::convert::Into::into), }; if is_blocked { - // blocked instances will only have an entry here if they had been federated with in the past. + // blocked instances will only have an entry here if they had been federated with in the + // past. blocked.push(i); } else if is_allowed { allowed.push(i.clone()); @@ -437,7 +458,7 @@ pub async fn send_password_reset_email( // Insert the row after successful send, to avoid using daily reset limit while // email sending is broken. let local_user_id = user.local_user.id; - PasswordResetRequest::create_token(pool, local_user_id, token.clone()).await?; + PasswordResetRequest::create(pool, local_user_id, token.clone()).await?; Ok(()) } @@ -533,25 +554,8 @@ pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult .try_get_with::<_, LemmyError>((), async { let urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; - let regexes = urls.iter().map(|url| { - let url = &url.url; - let parsed = Url::parse(url).expect("Coundln't parse URL."); - if url.ends_with('/') { - format!( - "({}://)?{}{}?", - parsed.scheme(), - escape(parsed.domain().expect("No domain.")), - escape(parsed.path()) - ) - } else { - format!( - "({}://)?{}{}", - parsed.scheme(), - escape(parsed.domain().expect("No domain.")), - escape(parsed.path()) - ) - } - }); + // The urls are already validated on saving, so just escape them. + let regexes = urls.iter().map(|url| escape(&url.url)); let set = RegexSet::new(regexes)?; Ok(set) @@ -660,15 +664,20 @@ pub async fn purge_image_posts_for_person( /// Delete a local_user's images async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { - if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), person_id).await { + if let Ok(Some(local_user)) = LocalUserView::read_person(&mut context.pool(), person_id).await { let pictrs_uploads = - LocalImage::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id).await?; + LocalImageView::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id) + .await?; // Delete their images for upload in pictrs_uploads { - delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, context) - .await - .ok(); + delete_image_from_pictrs( + &upload.local_image.pictrs_alias, + &upload.local_image.pictrs_delete_token, + context, + ) + .await + .ok(); } } Ok(()) @@ -700,7 +709,9 @@ pub async fn remove_user_data( ) -> LemmyResult<()> { let pool = &mut context.pool(); // Purge user images - let person = Person::read(pool, banned_person_id).await?; + let person = Person::read(pool, banned_person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; if let Some(avatar) = person.avatar { purge_image_from_pictrs(&avatar, context).await.ok(); } @@ -813,7 +824,9 @@ pub async fn remove_user_data_in_community( pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { let pool = &mut context.pool(); - let person = Person::read(pool, person_id).await?; + let person = Person::read(pool, person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; // Delete their local images, if they're a local user delete_local_user_images(person_id, context).await.ok(); @@ -959,8 +972,8 @@ pub async fn process_markdown_opt( /// A wrapper for `proxy_image_link` for use in tests. /// -/// The parameter `force_image_proxy` is the config value of `pictrs.image_proxy`. Its necessary to pass -/// as separate parameter so it can be changed in tests. +/// The parameter `force_image_proxy` is the config value of `pictrs.image_proxy`. Its necessary to +/// pass as separate parameter so it can be changed in tests. async fn proxy_image_link_internal( link: Url, image_mode: PictrsImageMode, @@ -970,13 +983,10 @@ async fn proxy_image_link_internal( if link.domain() == Some(&context.settings().hostname) { Ok(link.into()) } else if image_mode == PictrsImageMode::ProxyAllImages { - let proxied = format!( - "{}/api/v3/image_proxy?url={}", - context.settings().get_protocol_and_hostname(), - encode(link.as_str()) - ); + let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?; + RemoteImage::create(&mut context.pool(), vec![link]).await?; - Ok(Url::parse(&proxied)?.into()) + Ok(proxied.into()) } else { Ok(link.into()) } @@ -1030,6 +1040,17 @@ pub async fn proxy_image_link_opt_apub( } } +fn build_proxied_image_url( + link: &Url, + protocol_and_hostname: &str, +) -> Result { + Url::parse(&format!( + "{}/api/v3/image_proxy?url={}", + protocol_and_hostname, + encode(link.as_str()) + )) +} + #[cfg(test)] #[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 6b1c4ed30..2efd46964 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -9,11 +9,11 @@ use lemmy_api_common::{ check_community_user_action, check_post_deleted_or_removed, generate_local_apub_endpoint, - get_post, get_url_blocklist, is_mod_or_admin, local_site_to_slur_regex, process_markdown, + update_read_comments, EndpointType, }, }; @@ -28,7 +28,7 @@ use lemmy_db_schema::{ }, traits::{Crud, Likeable}, }; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, @@ -51,8 +51,19 @@ pub async fn create_comment( // Check for a community ban let post_id = data.post_id; - let post = get_post(post_id, &mut context.pool()).await?; - let community_id = post.community_id; + + // Read the full post view in order to get the comments count. + let post_view = PostView::read( + &mut context.pool(), + post_id, + Some(local_user_view.person.id), + true, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; + + let post = post_view.post; + let community_id = post_view.community.id; check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?; check_post_deleted_or_removed(&post)?; @@ -70,7 +81,8 @@ pub async fn create_comment( Comment::read(&mut context.pool(), parent_id).await.ok() } else { None - }; + } + .flatten(); // If there's a parent_id, check to make sure that comment is in that post // Strange issue where sometimes the post ID of the parent comment is incorrect @@ -163,6 +175,15 @@ pub async fn create_comment( ) .await?; + // Update the read comments, so your own new comment doesn't appear as a +1 unread + update_read_comments( + local_user_view.person.id, + post_id, + post_view.counts.comments + 1, + &mut context.pool(), + ) + .await?; + // If we're responding to a comment where we're the recipient, // (ie we're the grandparent, or the recipient of the parent comment_reply), // then mark the parent as read. @@ -172,7 +193,7 @@ pub async fn create_comment( let parent_id = parent.id; let comment_reply = CommentReply::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; - if let Ok(reply) = comment_reply { + if let Ok(Some(reply)) = comment_reply { CommentReply::update( &mut context.pool(), reply.id, @@ -185,7 +206,7 @@ pub async fn create_comment( // If the parent has PersonMentions mark them as read too let person_mention = PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; - if let Ok(mention) = person_mention { + if let Ok(Some(mention)) = person_mention { PersonMention::update( &mut context.pool(), mention.id, diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 00a8ea0c1..2b1a20f89 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -21,7 +21,9 @@ 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?; + let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; // Dont delete it if its already been deleted. if orig_comment.comment.deleted == data.deleted { diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 1355a7076..926472b94 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -25,7 +25,9 @@ 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?; + let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_mod_action( &local_user_view.person, @@ -35,6 +37,12 @@ pub async fn remove_comment( ) .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 { + return Err(LemmyErrorType::CouldntUpdateComment.into()); + } + // Do the remove let removed = data.removed; let updated_comment = Comment::update( diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index ff6f78804..695ededfe 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -36,7 +36,9 @@ 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?; + let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index 32d37d8ef..b0b6bea0e 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -46,7 +46,9 @@ pub async fn create_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let local_site = site_view.local_site; if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() { diff --git a/crates/api_crud/src/community/list.rs b/crates/api_crud/src/community/list.rs index 9c13ae89f..587b5cdfa 100644 --- a/crates/api_crud/src/community/list.rs +++ b/crates/api_crud/src/community/list.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::community_view::CommunityQuery; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn list_communities( @@ -14,7 +14,9 @@ pub async fn list_communities( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let is_admin = local_user_view .as_ref() .map(|luv| is_admin(luv).is_ok()) diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index bc0945ba1..33c6a47dd 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -43,7 +43,9 @@ pub async fn update_community( let description = process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?; is_valid_body_field(&data.description, false)?; - let old_community = Community::read(&mut context.pool(), data.community_id).await?; + let old_community = Community::read(&mut context.pool(), data.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; replace_image(&data.icon, &old_community.icon, &context).await?; replace_image(&data.banner, &old_community.banner, &context).await?; diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index cea0d5a31..21a989d3f 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -14,7 +14,6 @@ use lemmy_api_common::{ local_site_to_slur_regex, mark_post_as_read, process_markdown_opt, - proxy_image_link_opt_apub, EndpointType, }, }; @@ -75,7 +74,6 @@ pub async fn create_post( is_url_blocked(&url, &url_blocklist)?; check_url_scheme(&url)?; check_url_scheme(&custom_thumbnail)?; - let url = proxy_image_link_opt_apub(url, &context).await?; check_community_user_action( &local_user_view.person, @@ -85,7 +83,9 @@ pub async fn create_post( .await?; let community_id = data.community_id; - let community = Community::read(&mut context.pool(), community_id).await?; + let community = Community::read(&mut context.pool(), community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if community.posting_restricted_to_mods { let community_id = data.community_id; let is_mod = CommunityModeratorView::is_community_moderator( @@ -123,7 +123,7 @@ pub async fn create_post( let post_form = PostInsertForm::builder() .name(data.name.trim().to_string()) - .url(url) + .url(url.map(Into::into)) .body(body) .alt_text(data.alt_text.clone()) .community_id(data.community_id) @@ -160,7 +160,8 @@ pub async fn create_post( |post| Some(SendActivityData::CreatePost(post)), Some(local_site), context.reset_request_count(), - ); + ) + .await?; // They like their own post by default let person_id = local_user_view.person.id; @@ -175,7 +176,6 @@ pub async fn create_post( .await .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; - // Mark the post as read mark_post_as_read(person_id, post_id, &mut context.pool()).await?; if let Some(url) = updated_post.url.clone() { diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs index e07ff7723..696566c8e 100644 --- a/crates/api_crud/src/post/delete.rs +++ b/crates/api_crud/src/post/delete.rs @@ -21,7 +21,9 @@ pub async fn delete_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; // Dont delete it if its already been deleted. if orig_post.deleted == data.deleted { diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index cb4ba7e9f..506b97299 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -2,10 +2,9 @@ use actix_web::web::{Data, Json, Query}; use lemmy_api_common::{ context::LemmyContext, post::{GetPost, GetPostResponse}, - utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read}, + utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read, update_read_comments}, }; use lemmy_db_schema::{ - aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, source::{comment::Comment, post::Post}, traits::Crud, }; @@ -14,7 +13,7 @@ use lemmy_db_views::{ structs::{LocalUserView, PostView, SiteView}, }; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] pub async fn get_post( @@ -22,7 +21,9 @@ pub async fn get_post( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; check_private_instance(&local_user_view, &local_site.local_site)?; @@ -33,15 +34,19 @@ pub async fn get_post( id } else if let Some(comment_id) = data.comment_id { Comment::read(&mut context.pool(), comment_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindPost)? + .await? + .ok_or(LemmyErrorType::CouldntFindComment)? .post_id } else { Err(LemmyErrorType::CouldntFindPost)? }; // Check to see if the person is a mod or admin, to show deleted / removed - let community_id = Post::read(&mut context.pool(), post_id).await?.community_id; + let community_id = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)? + .community_id; + let is_mod_or_admin = is_mod_or_admin_opt( &mut context.pool(), local_user_view.as_ref(), @@ -51,13 +56,20 @@ pub async fn get_post( .is_ok(); let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindPost)?; + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; - // Mark the post as read let post_id = post_view.post.id; if let Some(person_id) = person_id { mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + + update_read_comments( + person_id, + post_id, + post_view.counts.comments, + &mut context.pool(), + ) + .await?; } // Necessary for the sidebar subscribed @@ -67,23 +79,8 @@ pub async fn get_post( person_id, is_mod_or_admin, ) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; - - // Insert into PersonPostAggregates - // to update the read_comments count - if let Some(person_id) = person_id { - let read_comments = post_view.counts.comments; - let person_post_agg_form = PersonPostAggregatesForm { - person_id, - post_id, - read_comments, - ..PersonPostAggregatesForm::default() - }; - PersonPostAggregates::upsert(&mut context.pool(), &person_post_agg_form) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindPost)?; - } + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index 37f3f6148..682ed75d3 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -16,7 +16,7 @@ use lemmy_db_schema::{ traits::{Crud, Reportable}, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn remove_post( @@ -25,7 +25,9 @@ pub async fn remove_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; check_community_mod_action( &local_user_view.person, diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index 48a1b6523..4b4bd9845 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -11,7 +11,6 @@ use lemmy_api_common::{ get_url_blocklist, local_site_to_slur_regex, process_markdown_opt, - proxy_image_link_opt_apub, }, }; use lemmy_db_schema::{ @@ -70,7 +69,9 @@ pub async fn update_post( check_url_scheme(&custom_thumbnail)?; let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; check_community_user_action( &local_user_view.person, @@ -84,11 +85,6 @@ pub async fn update_post( Err(LemmyErrorType::NoPostEditAllowed)? } - let url = match url { - Some(url) => Some(proxy_image_link_opt_apub(Some(url), &context).await?), - _ => Default::default(), - }; - let language_id = data.language_id; CommunityLanguage::is_allowed_community_language( &mut context.pool(), @@ -99,7 +95,7 @@ pub async fn update_post( let post_form = PostUpdateForm { name: data.name.clone(), - url, + url: Some(url.map(Into::into)), body: diesel_option_overwrite(body), alt_text: diesel_option_overwrite(data.alt_text.clone()), nsfw: data.nsfw, @@ -119,7 +115,8 @@ pub async fn update_post( |post| Some(SendActivityData::UpdatePost(post)), Some(local_site), context.reset_request_count(), - ); + ) + .await?; build_post_response( context.deref(), diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 1fe4f9b48..e977a6c86 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -76,12 +76,16 @@ pub async fn create_private_message( .await .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?; - let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?; + let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; // Send email to the local recipient, if one exists if view.recipient.local { let recipient_id = data.recipient_id; - let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?; + let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let lang = get_interface_language(&local_recipient); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let sender_name = &local_user_view.person.name; diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index 936ff57b8..dc028ff41 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -20,7 +20,9 @@ pub async fn delete_private_message( ) -> LemmyResult> { // Checking permissions let private_message_id = data.private_message_id; - let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; + let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; if local_user_view.person.id != orig_private_message.creator_id { Err(LemmyErrorType::EditPrivateMessageNotAllowed)? } @@ -45,7 +47,9 @@ pub async fn delete_private_message( ) .await?; - let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; + let view = PrivateMessageView::read(&mut context.pool(), private_message_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; Ok(Json(PrivateMessageResponse { private_message_view: view, })) diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 765a33053..2842fea65 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -30,7 +30,9 @@ pub async fn update_private_message( // Checking permissions let private_message_id = data.private_message_id; - let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; + let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; if local_user_view.person.id != orig_private_message.creator_id { Err(LemmyErrorType::EditPrivateMessageNotAllowed)? } @@ -54,7 +56,9 @@ pub async fn update_private_message( .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; - let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; + let view = PrivateMessageView::read(&mut context.pool(), private_message_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPrivateMessage)?; ActivityChannel::submit_activity( SendActivityData::UpdatePrivateMessage(view.clone()), diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 1c2fed3ed..466c7ff1d 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -129,7 +129,9 @@ pub async fn create_site( LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?; - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let new_taglines = data.taglines.clone(); let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index c8ad866a3..69e82007c 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -41,7 +41,9 @@ pub async fn get_site( // This data is independent from the user account so we can cache it across requests let mut site_response = CACHE .try_get_with::<_, LemmyError>((), async { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let admins = PersonView::admins(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index adf44fad4..7efa9b568 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -52,7 +52,9 @@ pub async fn update_site( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let local_site = site_view.local_site; let site = site_view.site; @@ -154,7 +156,8 @@ pub async fn update_site( // TODO can't think of a better way to do this. // If the server suddenly requires email verification, or required applications, no old users // will be able to log in. It really only wants this to be a requirement for NEW signups. - // So if it was set from false, to true, you need to update all current users columns to be verified. + // So if it was set from false, to true, you need to update all current users columns to be + // verified. let old_require_application = local_site.registration_mode == RegistrationMode::RequireApplication; @@ -181,7 +184,9 @@ pub async fn update_site( let new_taglines = data.taglines.clone(); let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let rate_limit_config = local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 1f81d4324..bf064fb2e 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -45,7 +45,9 @@ pub async fn register( req: HttpRequest, context: Data, ) -> LemmyResult> { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let local_site = site_view.local_site; let require_registration_application = local_site.registration_mode == RegistrationMode::RequireApplication; @@ -140,12 +142,17 @@ pub async fn register( .map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string()) .collect(); + // Show nsfw content if param is true, or if content_warning exists + let show_nsfw = data + .show_nsfw + .unwrap_or(site_view.site.content_warning.is_some()); + // 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(data.show_nsfw)) + .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)) @@ -190,7 +197,8 @@ pub async fn register( verify_email_sent: false, }; - // Log the user in directly if the site is not setup, or email verification and application aren't required + // Log the user in directly if the site is not setup, or email verification and application aren't + // required if !local_site.site_setup || (!require_registration_application && !local_site.require_email_verification) { diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index 4c3189a09..a9c6f657a 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -44,7 +44,7 @@ once_cell = { workspace = true } moka.workspace = true serde_with.workspace = true html2md = "0.2.14" -html2text = "0.6.0" +html2text = "0.12.5" stringreader = "0.1.1" enum_delegate = "0.2.0" diff --git a/crates/apub/assets/discourse/objects/group.json b/crates/apub/assets/discourse/objects/group.json new file mode 100644 index 000000000..5ed2c8c05 --- /dev/null +++ b/crates/apub/assets/discourse/objects/group.json @@ -0,0 +1,22 @@ +{ + "id": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146", + "type": "Group", + "updated": "2024-04-05T12:49:51Z", + "url": "https://socialhub.activitypub.rocks/c/meeting/threadiverse-wg/88", + "name": "Threadiverse Working Group (SocialHub)", + "inbox": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/inbox", + "outbox": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/outbox", + "followers": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/followers", + "preferredUsername": "threadiverse-wg", + "publicKey": { + "id": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146#main-key", + "owner": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJi4iAcW6bPiHVCxT9p0\n8DVnrDDO4QtLNy7bpRFdMFifmmmXprsuAi9D2MSwbhH49V54HtIkxBpKd2IR/UD8\nmhMDY4CNI9FHpjqLw0wtkzxcqF9urSqhn0/vWX+9oxyhIgQS5KMiIkYDMJiAc691\niEcZ8LCran23xIGl6Dk54Nr3TqTMLcjDhzQYUJbxMrLq5/knWqOKG3IF5OxK+9ZZ\n1wxDF872eJTxJLkmpag+WYNtHzvB2SGTp8j5IF1/pZ9J1c3cpYfaeolTch/B/GQn\najCB4l27U52rIIObxJqFXSY8wHyd0aAmNmxzPZ7cduRlBDhmI40cAmnCV1YQPvpk\nDwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://socialhub.activitypub.rocks/uploads/default/original/1X/8faac84234dc73d074dadaa2bcf24dc746b8647f.png" + }, + "@context": "https://www.w3.org/ns/activitystreams" +} diff --git a/crates/apub/assets/discourse/objects/page.json b/crates/apub/assets/discourse/objects/page.json new file mode 100644 index 000000000..c020ce9f5 --- /dev/null +++ b/crates/apub/assets/discourse/objects/page.json @@ -0,0 +1,13 @@ +{ + "id": "https://socialhub.activitypub.rocks/ap/object/1899f65c062200daec50a4c89ed76dc9", + "type": "Note", + "audience": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146", + "published": "2024-04-13T14:36:19Z", + "updated": "2024-04-13T14:36:19Z", + "url": "https://socialhub.activitypub.rocks/t/our-next-meeting/4079/1", + "attributedTo": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1", + "name": "Our next meeting", + "context": "https://socialhub.activitypub.rocks/ap/collection/8850f6e85b57c490da915a5dfbbd5045", + "content": "

Last Meeting

\n

Recording

\nhttps://us06web.zoom.us/rec/share/4hGBTvgXJPlu8UkjkkxVARypNg5DH0eeaKlIBv71D4G3lokYyrCrg7cqBCJmL109.FsHYTZDlVvZXrgcn?startTime=1712254114000\nPasscode: z+1*4pUB\n

Minutes

\nTo refresh your memory, you can read the minutes of last week's meeting NodeBB is at this year's FediForum, and one of the breakout sessions centred around the Theadiverse, the subset of ActivityPub-enabled applications built around a topic-centric model of content representation.

\n

Some of the topic touched upon included:

\n
    \n
  • Aligning on a standard representation for collections of Notes
  • \n
  • FEP-1b12 — Group federation and implementation thereof by Lemmy, et al.
  • \n
  • Offering a comparatively more feature-rich experience vis-a-vis restrictions re: microblogging
  • \n
  • Going forward: collaborating on building compatible threadiverse implementations
  • \n
\n

The main action item involved the genesis of an informal working group for the threadiverse, in order to align our disparate implementations toward a common path.

\n

We intend to meet monthly at first, with the first meeting likely sometime early-to-mid April.

\n

The topic of the first WG call is: Representation of the higherlevel collection of Notes (posts, etc.) — Article vs. Page, etc?

\n

Interested?

\n
\n
\n

As an aside, I'd love to try something new and attempt tokeep as much of this as I can on the social web. Can you do me a favour and boost this to your followers?

\n", + "source": { + "content": "NodeBB is at this year's FediForum, and one of the breakout sessions centred around **the Theadiverse**, the subset of ActivityPub-enabled applications built around a topic-centric model of content representation.\n\nSome of the topic touched upon included:\n\n* Aligning on a standard representation for collections of Notes\n* FEP-1b12 — Group federation and implementation thereof by Lemmy, et al.\n* Offering a comparatively more feature-rich experience vis-a-vis restrictions re: microblogging\n* Going forward: collaborating on building compatible threadiverse implementations\n\nThe main action item involved **the genesis of an informal working group for the threadiverse**, in order to align our disparate implementations toward a common path.\n\nWe intend to meet monthly at first, with the first meeting likely sometime early-to-mid April.\n\nThe topic of the first WG call is: **Representation of the higher level collection of Notes (posts, etc.) — Article vs. Page, etc?**\n\nInterested?\n\n* Publicly reply to this post (NodeBB does not support non-public postsat this time) if you'd like to join the list\n* If you prefer to remain private, please email julian@nodebb.org\n\n----\n\nAs an aside, I'd love to try something new and attempt to keep as much of this as I can on the social web. Can you do me a favour and boost this to your followers?", + "mediaType": "text/markdown" + }, + "tag": [ + { + "type": "Hashtag", + "href": "https://community.nodebb.org/tags/fediforum", + "name": "#fediforum" + }, + { + "type": "Hashtag", + "href": "https://community.nodebb.org/tags/activitypub", + "name": "#activitypub" + }, + { + "type": "Hashtag", + "href": "https://community.nodebb.org/tags/threadiverse", + "name": "#threadiverse" + } + ], + "attachment": [] +} diff --git a/crates/apub/assets/nodebb/objects/person.json b/crates/apub/assets/nodebb/objects/person.json new file mode 100644 index 000000000..aa2ee5c8c --- /dev/null +++ b/crates/apub/assets/nodebb/objects/person.json @@ -0,0 +1,29 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://community.nodebb.org/uid/2", + "url": "https://community.nodebb.org/user/julian", + "followers": "https://community.nodebb.org/uid/2/followers", + "following": "https://community.nodebb.org/uid/2/following", + "inbox": "https://community.nodebb.org/uid/2/inbox", + "outbox": "https://community.nodebb.org/uid/2/outbox", + "sharedInbox": "https://community.nodebb.org/inbox", + "type": "Person", + "name": "julian", + "preferredUsername": "julian", + "summary": "Hi! I'm Julian, one of the co-founders of NodeBB, the forum software you are using right now.\r\n\r\nI started this company with two colleagues, Baris and Andrew, in 2013, and have been doing the startup thing since (although I think at some point along the way we stopped being a startup and just became a boring ol' small business).\r\n\r\nIn my free time I rock climb, cycle, and lift weights. I live just outside Toronto, Canada, with my wife and three children.", + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1701457270279.jpeg" + }, + "image": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profilecover-1649468285913.jpeg" + }, + "publicKey": { + "id": "https://community.nodebb.org/uid/2#key", + "owner": "https://community.nodebb.org/uid/2", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzEr0sFdATahQzprS4EOT\nZq+KMc6UTbt2GDP20OrQi/P5AXAbMaQiRCRdGWhYGjnH0jicn5NnozNxRo+HchJT\nV6NOHxpsxqPCoaLeoBkhfhbSCLr2Gzil6mmfqf9TjnI7A7ZTtCc0G+n0ztyL9HwL\nkEAI178l2gckk4XKKYnEd+dyiIevExrq/ROLgwW1o428FZvlF5amKxhpVUEygRU8\nCd1hqWYs+xYDOJURCP5qEx/MmRPpV/yGMTMyF+/gcQc0TUZnhWAM2E4M+aq3aKh6\nJP/vsry+5YZPUaPWfopbT5Ijyt6ZSElp6Avkg56eTz0a5SRcjCVS6IFVPwiLlzOe\nYwIDAQAB\n-----END PUBLIC KEY-----\n" + } +} diff --git a/crates/apub/assets/wordpress/activities/announce.json b/crates/apub/assets/wordpress/activities/announce.json new file mode 100644 index 000000000..985dec307 --- /dev/null +++ b/crates/apub/assets/wordpress/activities/announce.json @@ -0,0 +1,49 @@ +{ + "@context": ["https://www.w3.org/ns/activitystreams"], + "id": "https://pfefferle.org/lemmy-part-4/#activity#activity", + "type": "Announce", + "audience": "https://pfefferle.org/@pfefferle.org", + "published": "2024-05-03T12:32:29Z", + "updated": "2024-05-06T08:20:33Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://pfefferle.org/wp-json/activitypub/1.0/actors/1/followers" + ], + "cc": [], + "object": { + "id": "https://pfefferle.org/lemmy-part-4/#activity", + "type": "Update", + "audience": "https://pfefferle.org/@pfefferle.org", + "published": "2024-05-03T12:32:29Z", + "updated": "2024-05-06T08:20:33Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://pfefferle.org/wp-json/activitypub/1.0/actors/1/followers" + ], + "cc": [], + "object": { + "id": "https://pfefferle.org/lemmy-part-4/", + "type": "Article", + "attachment": [], + "attributedTo": "https://pfefferle.org/author/pfefferle/", + "audience": "https://pfefferle.org/@pfefferle.org", + "content": "\u003Cp\u003EIdentifies one or more entities that represent the total population of entities for which the object can considered to be relevant. Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant. \u003C/p\u003E", + "contentMap": { + "en": "\u003Cp\u003EIdentifies one or more entities that represent the total population of entities for which the object can considered to be relevant. Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant. \u003C/p\u003E" + }, + "name": "Lemmy (Part 4)", + "published": "2024-05-03T12:32:29Z", + "summary": "Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant. Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object [...]", + "tag": [], + "updated": "2024-05-06T08:20:33Z", + "url": "https://pfefferle.org/lemmy-part-4/", + "to": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://pfefferle.org/wp-json/activitypub/1.0/actors/1/followers" + ], + "cc": [] + }, + "actor": "https://pfefferle.org/author/pfefferle/" + }, + "actor": "https://pfefferle.org/@pfefferle.org" +} diff --git a/crates/apub/assets/wordpress/objects/group.json b/crates/apub/assets/wordpress/objects/group.json new file mode 100644 index 000000000..35f2af0c4 --- /dev/null +++ b/crates/apub/assets/wordpress/objects/group.json @@ -0,0 +1,66 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + "https://purl.archive.org/socialweb/webfinger", + { + "schema": "http://schema.org#", + "toot": "http://joinmastodon.org/ns#", + "webfinger": "https://webfinger.net/#", + "lemmy": "https://join-lemmy.org/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "Hashtag": "as:Hashtag", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "featuredTags": { + "@id": "toot:featuredTags", + "@type": "@id" + }, + "moderators": { + "@id": "lemmy:moderators", + "@type": "@id" + }, + "postingRestrictedToMods": "lemmy:postingRestrictedToMods", + "discoverable": "toot:discoverable", + "indexable": "toot:indexable", + "resource": "webfinger:resource" + } + ], + "id": "https://pfefferle.org/@pfefferle.org", + "type": "Group", + "attachment": [], + "attributedTo": "https://pfefferle.org/wp-json/activitypub/1.0/collections/moderators", + "name": "Matthias Pfefferle", + "icon": { + "type": "Image", + "url": "https://pfefferle.org/wp-content/uploads/2023/06/cropped-BeLItBV-_400x400.jpg" + }, + "published": "2024-04-03T16:58:22Z", + "summary": "

Webworker, blogger und podcaster

\n", + "tag": [], + "url": "https://pfefferle.org/@pfefferle.org", + "inbox": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/inbox", + "outbox": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/outbox", + "following": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/following", + "followers": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/followers", + "preferredUsername": "pfefferle.org", + "endpoints": { + "sharedInbox": "https://pfefferle.org/wp-json/activitypub/1.0/inbox" + }, + "publicKey": { + "id": "https://pfefferle.org/@pfefferle.org#main-key", + "owner": "https://pfefferle.org/@pfefferle.org", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuq8xeLMFcaCwPFBhgMRE\n/dDh2XKoNXFXnixctmK8BXSuuLMxucm3I/8NyhIvb3LqU+uP1fO8F0ecUbk2sN+x\nKag5vIV6yKXzJ8ILMWQ9AaELpXDmMZqL0zal0LUJRAOkDgPDovDAoq6tx++yDoV0\njdVbf9CoZKit1cz2ZrEuE5dswq3J/z9+c6POkhCkWEX5TPJzkOrmnjkvrXxGHUJ2\nA3+P+VaZhd5cmvqYosSpYNJshxCdev12pIF78OnYLiYiyXlgGHU+7uQR0M4tTcij\n6cUdLkms9m+b6H3ctXntPn410e5YLFPldjAYzQB5wHVdFZsWtyrbqfYdCa+KkKpA\nvwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "manuallyApprovesFollowers": false, + "featured": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/collections/featured", + "moderators": "https://pfefferle.org/wp-json/activitypub/1.0/collections/moderators", + "discoverable": true, + "indexable": true, + "webfinger": "pfefferle.org@pfefferle.org", + "postingRestrictedToMods": true +} diff --git a/crates/apub/assets/wordpress/objects/note.json b/crates/apub/assets/wordpress/objects/note.json new file mode 100644 index 000000000..5b4b24da0 --- /dev/null +++ b/crates/apub/assets/wordpress/objects/note.json @@ -0,0 +1,24 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "Hashtag": "as:Hashtag" + } + ], + "id": "https://pfefferle.org?c=148", + "type": "Note", + "attributedTo": "https://pfefferle.org/author/pfefferle/", + "content": "

Nice! Hello from WordPress!

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

Nice! Hello from WordPress!

" + }, + "inReplyTo": "https://socialhub.activitypub.rocks/ap/object/ce040f1ead95964f6dbbf1084b81432d", + "published": "2024-04-30T15:21:13Z", + "tag": [], + "url": "https://pfefferle.org?c=148", + "to": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://pfefferle.org/wp-json/activitypub/1.0/users/0/followers" + ], + "cc": [] +} diff --git a/crates/apub/assets/wordpress/objects/page.json b/crates/apub/assets/wordpress/objects/page.json new file mode 100644 index 000000000..1e8f4be3c --- /dev/null +++ b/crates/apub/assets/wordpress/objects/page.json @@ -0,0 +1,26 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "Hashtag": "as:Hashtag" + } + ], + "id": "https://pfefferle.org/this-is-a-test-federation/", + "type": "Article", + "attachment": [], + "attributedTo": "https://pfefferle.org/author/pfefferle/", + "content": "

with Discource!

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

with Discource!

" + }, + "name": "This is a test-federation", + "published": "2024-04-30T15:16:41Z", + "summary": "with Discource! [...]", + "tag": [], + "url": "https://pfefferle.org/this-is-a-test-federation/", + "to": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://pfefferle.org/wp-json/activitypub/1.0/users/1/followers" + ], + "cc": [] +} diff --git a/crates/apub/assets/wordpress/objects/person.json b/crates/apub/assets/wordpress/objects/person.json new file mode 100644 index 000000000..6c74b53bf --- /dev/null +++ b/crates/apub/assets/wordpress/objects/person.json @@ -0,0 +1,74 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + "https://purl.archive.org/socialweb/webfinger", + { + "schema": "http://schema.org#", + "toot": "http://joinmastodon.org/ns#", + "webfinger": "https://webfinger.net/#", + "lemmy": "https://join-lemmy.org/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "Hashtag": "as:Hashtag", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "featuredTags": { + "@id": "toot:featuredTags", + "@type": "@id" + }, + "moderators": { + "@id": "lemmy:moderators", + "@type": "@id" + }, + "postingRestrictedToMods": "lemmy:postingRestrictedToMods", + "discoverable": "toot:discoverable", + "indexable": "toot:indexable", + "resource": "webfinger:resource" + } + ], + "id": "https://pfefferle.org/author/pfefferle/", + "type": "Person", + "attachment": [ + { + "type": "PropertyValue", + "name": "Blog", + "value": "pfefferle.org" + }, + { + "type": "PropertyValue", + "name": "Profile", + "value": "pfefferle.org" + } + ], + "name": "Matthias Pfefferle", + "icon": { + "type": "Image", + "url": "https://secure.gravatar.com/avatar/a2bdca7870e859658cece96c044b3be5?s=120&d=mm&r=g" + }, + "published": "2014-02-10T15:23:08Z", + "summary": "

Ich arbeite als Open Web Lead für Automattic.

\n", + "tag": [], + "url": "https://pfefferle.org/author/pfefferle/", + "inbox": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/inbox", + "outbox": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/outbox", + "following": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/following", + "followers": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/followers", + "preferredUsername": "matthias", + "endpoints": { + "sharedInbox": "https://pfefferle.org/wp-json/activitypub/1.0/inbox" + }, + "publicKey": { + "id": "https://pfefferle.org/author/pfefferle/#main-key", + "owner": "https://pfefferle.org/author/pfefferle/", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTA5RA40nOsso04RSwyX\nHXTojRPUMlIlArDcSy3M5GUJp9/xbxSUOdBjqd31KKB1GIi3vrLmD1Qi/ZqS95Qy\nw2Zd3xOsCg+o9bsyOG+O6Y8Lu+HEB5JKLUbNHdiSviakJ8wGadH9Wm4WIiN20y+q\n/u6lgxgiWfZ2CFCN6SOc28fUKi9NmKvXK+M12BhFfy1tC5KWXKDm0UbfI1+dmqhR\n3Ffe6vEsCI/YIVVdWxQ9kouOd0XSHOGdslktkepRO7IP9i9TdwyeCa0WWRoeO5Wa\ntVpc1Y0WuNbTM2ksIXTg0G+rO1/6KO/hrHnGu3RCfb/ZIHK5L/aWYb9B3PG3LyKV\n+wIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "manuallyApprovesFollowers": false, + "featured": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/collections/featured", + "discoverable": true, + "indexable": true, + "webfinger": "matthias@pfefferle.org" +} diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index f68301be1..48408c4fb 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -39,7 +39,10 @@ use lemmy_db_schema::{ }, traits::{Bannable, Crud, Followable}, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, +}; use url::Url; impl BlockUser { @@ -129,7 +132,11 @@ impl ActivityHandler for BlockUser { verify_is_public(&self.to, &self.cc)?; match self.target.dereference(context).await? { SiteOrCommunity::Site(site) => { - let domain = self.object.inner().domain().expect("url needs domain"); + let domain = self + .object + .inner() + .domain() + .ok_or(LemmyErrorType::UrlWithoutDomain)?; if context.settings().hostname == domain { return Err( anyhow!("Site bans from remote instance can't affect user's home instance").into(), diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs index 51d8bf5e0..ced50e2de 100644 --- a/crates/apub/src/activities/block/mod.rs +++ b/crates/apub/src/activities/block/mod.rs @@ -23,7 +23,10 @@ use lemmy_db_schema::{ utils::DbPool, }; use lemmy_db_views::structs::SiteView; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, +}; use serde::Deserialize; use url::Url; @@ -134,7 +137,13 @@ pub(crate) async fn send_ban_from_site( expires: Option, context: Data, ) -> LemmyResult<()> { - let site = SiteOrCommunity::Site(SiteView::read_local(&mut context.pool()).await?.site.into()); + let site = SiteOrCommunity::Site( + SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)? + .site + .into(), + ); let expires = check_expire_time(expires)?; // if the action affects a local user, federate to other instances @@ -174,6 +183,7 @@ pub(crate) async fn send_ban_from_community( ) -> LemmyResult<()> { let community: ApubCommunity = Community::read(&mut context.pool(), community_id) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let expires = check_expire_time(data.expires)?; diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index 9a3928882..aebf28217 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -94,7 +94,12 @@ impl AnnounceActivity { actor: community.id().into(), to: vec![public()], object: IdOrNestedObject::NestedObject(object), - cc: vec![community.followers_url.clone().into()], + cc: community + .followers_url + .clone() + .map(Into::into) + .into_iter() + .collect(), kind: AnnounceType::Announce, id, }) diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs index 34c46cf6b..4048a1469 100644 --- a/crates/apub/src/activities/community/collection_add.rs +++ b/crates/apub/src/activities/community/collection_add.rs @@ -36,7 +36,10 @@ use lemmy_db_schema::{ }, traits::{Crud, Joinable}, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, +}; use url::Url; impl CollectionAdd { @@ -126,15 +129,17 @@ impl ActivityHandler for CollectionAdd { async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let (community, collection_type) = - Community::get_by_collection_url(&mut context.pool(), &self.target.into()).await?; + Community::get_by_collection_url(&mut context.pool(), &self.target.into()) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; match collection_type { CollectionType::Moderators => { let new_mod = ObjectId::::from(self.object) .dereference(context) .await?; - // If we had to refetch the community while parsing the activity, then the new mod has already - // been added. Skip it here as it would result in a duplicate key error. + // If we had to refetch the community while parsing the activity, then the new mod has + // already been added. Skip it here as it would result in a duplicate key error. let new_mod_id = new_mod.id; let moderated_communities = CommunityModerator::get_person_moderated_communities(&mut context.pool(), new_mod_id) @@ -183,9 +188,11 @@ pub(crate) async fn send_add_mod_to_community( let actor: ApubPerson = actor.into(); let community: ApubCommunity = Community::read(&mut context.pool(), community_id) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let updated_mod: ApubPerson = Person::read(&mut context.pool(), updated_mod_id) .await? + .ok_or(LemmyErrorType::CouldntFindPerson)? .into(); if added { CollectionAdd::send_add_mod(&community, &updated_mod, &actor, &context).await @@ -204,6 +211,7 @@ pub(crate) async fn send_feature_post( let post: ApubPost = post.into(); let community = Community::read(&mut context.pool(), post.community_id) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); if featured { CollectionAdd::send_add_featured_post(&community, &post, &actor, &context).await diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs index 90df1fd14..634ca526c 100644 --- a/crates/apub/src/activities/community/collection_remove.rs +++ b/crates/apub/src/activities/community/collection_remove.rs @@ -31,7 +31,10 @@ use lemmy_db_schema::{ }, traits::{Crud, Joinable}, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, +}; use url::Url; impl CollectionRemove { @@ -121,7 +124,9 @@ impl ActivityHandler for CollectionRemove { async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let (community, collection_type) = - Community::get_by_collection_url(&mut context.pool(), &self.target.into()).await?; + Community::get_by_collection_url(&mut context.pool(), &self.target.into()) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; match collection_type { CollectionType::Moderators => { let remove_mod = ObjectId::::from(self.object) diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs index ba3e16417..322cd88c2 100644 --- a/crates/apub/src/activities/community/lock_page.rs +++ b/crates/apub/src/activities/community/lock_page.rs @@ -26,12 +26,16 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::Community, + moderator::{ModLockPost, ModLockPostForm}, person::Person, post::{Post, PostUpdateForm}, }, traits::Crud, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, +}; use url::Url; #[async_trait::async_trait] @@ -57,12 +61,22 @@ impl ActivityHandler for LockPage { } async fn receive(self, context: &Data) -> Result<(), Self::Error> { + insert_received_activity(&self.id, context).await?; + let locked = Some(true); let form = PostUpdateForm { - locked: Some(true), + locked, ..Default::default() }; let post = self.object.dereference(context).await?; Post::update(&mut context.pool(), post.id, &form).await?; + + let form = ModLockPostForm { + mod_person_id: self.actor.dereference(context).await?.id, + post_id: post.id, + locked, + }; + ModLockPost::create(&mut context.pool(), &form).await?; + Ok(()) } } @@ -91,12 +105,21 @@ impl ActivityHandler for UndoLockPage { async fn receive(self, context: &Data) -> Result<(), Self::Error> { insert_received_activity(&self.id, context).await?; + let locked = Some(false); let form = PostUpdateForm { - locked: Some(false), + locked, ..Default::default() }; let post = self.object.object.dereference(context).await?; Post::update(&mut context.pool(), post.id, &form).await?; + + let form = ModLockPostForm { + mod_person_id: self.actor.dereference(context).await?.id, + post_id: post.id, + locked, + }; + ModLockPost::create(&mut context.pool(), &form).await?; + Ok(()) } } @@ -109,6 +132,7 @@ pub(crate) async fn send_lock_post( ) -> LemmyResult<()> { let community: ApubCommunity = Community::read(&mut context.pool(), post.community_id) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let id = generate_activity_id( LockType::Lock, diff --git a/crates/apub/src/activities/community/mod.rs b/crates/apub/src/activities/community/mod.rs index a47dec2bd..59b8fadbb 100644 --- a/crates/apub/src/activities/community/mod.rs +++ b/crates/apub/src/activities/community/mod.rs @@ -24,13 +24,14 @@ pub mod update; /// /// Activities are sent to the community itself if it lives on another instance. If the community /// is local, the activity is directly wrapped into Announce and sent to community followers. -/// Activities are also sent to those who follow the actor (with exception of moderation activities). +/// Activities are also sent to those who follow the actor (with exception of moderation +/// activities). /// /// * `activity` - The activity which is being sent /// * `actor` - The user who is sending the activity /// * `community` - Community inside which the activity is sent -/// * `inboxes` - Any additional inboxes the activity should be sent to (for example, -/// to the user who is being promoted to moderator) +/// * `inboxes` - Any additional inboxes the activity should be sent to (for example, to the user +/// who is being promoted to moderator) /// * `is_mod_activity` - True for things like Add/Mod, these are not sent to user followers pub(crate) async fn send_activity_in_community( activity: AnnouncableActivities, diff --git a/crates/apub/src/activities/community/report.rs b/crates/apub/src/activities/community/report.rs index 4966add34..d1bec0b75 100644 --- a/crates/apub/src/activities/community/report.rs +++ b/crates/apub/src/activities/community/report.rs @@ -29,7 +29,10 @@ use lemmy_db_schema::{ }, traits::{Crud, Reportable}, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, +}; use url::Url; impl Report { @@ -67,7 +70,9 @@ impl Report { PostOrComment::Post(p) => p.creator_id, PostOrComment::Comment(c) => c.creator_id, }; - let object_creator = Person::read(&mut context.pool(), object_creator_id).await?; + let object_creator = Person::read(&mut context.pool(), object_creator_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let object_creator_site: Option = Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id) .await? diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index 3457e6c24..f507b3425 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -105,7 +105,7 @@ impl ActivityHandler for UpdateCommunity { last_refreshed_at: Some(naive_now()), icon: Some(self.object.icon.map(|i| i.url.into())), banner: Some(self.object.image.map(|i| i.url.into())), - followers_url: Some(self.object.followers.into()), + followers_url: self.object.followers.map(Into::into), inbox_url: Some(self.object.inbox.into()), shared_inbox_url: Some(self.object.endpoints.map(|e| e.shared_inbox.into())), moderators_url: self.object.attributed_to.map(Into::into), diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index 8fbbe42b3..2406d2eb3 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -19,7 +19,7 @@ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, kinds::public, - protocol::verification::verify_domains_match, + protocol::verification::{verify_domains_match, verify_urls_match}, traits::{ActivityHandler, Actor, Object}, }; use lemmy_api_common::{ @@ -42,6 +42,7 @@ use lemmy_db_schema::{ use lemmy_utils::{ error::{LemmyError, LemmyResult}, utils::mention::scrape_text_for_mentions, + LemmyErrorType, }; use url::Url; @@ -55,11 +56,17 @@ impl CreateOrUpdateNote { ) -> LemmyResult<()> { // TODO: might be helpful to add a comment method to retrieve community directly let post_id = comment.post_id; - let post = Post::read(&mut context.pool(), post_id).await?; + let post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; let community_id = post.community_id; - let person: ApubPerson = Person::read(&mut context.pool(), person_id).await?.into(); + let person: ApubPerson = Person::read(&mut context.pool(), person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)? + .into(); let community: ApubCommunity = Community::read(&mut context.pool(), community_id) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let id = generate_activity_id( @@ -126,6 +133,7 @@ impl ActivityHandler for CreateOrUpdateNote { verify_domains_match(self.actor.inner(), self.object.id.inner())?; check_community_deleted_or_removed(&community)?; check_post_deleted_or_removed(&post)?; + verify_urls_match(self.actor.inner(), self.object.attributed_to.inner())?; ApubComment::verify(&self.object, self.actor.inner(), context).await?; Ok(()) @@ -168,7 +176,8 @@ impl ActivityHandler for CreateOrUpdateNote { // Although mentions could be gotten from the post tags (they are included there), or the ccs, // Its much easier to scrape them from the comment body, since the API has to do that // anyway. - // TODO: for compatibility with other projects, it would be much better to read this from cc or tags + // 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?; Ok(()) diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs index eb59e054a..e8bfc36e6 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -4,7 +4,6 @@ use crate::{ community::send_activity_in_community, generate_activity_id, verify_is_public, - verify_mod_action, verify_person_in_community, }, activity_lists::AnnouncableActivities, @@ -66,23 +65,25 @@ impl CreateOrUpdatePage { kind: CreateOrUpdateType, context: Data, ) -> LemmyResult<()> { - let post = ApubPost(post); let community_id = post.community_id; - let person: ApubPerson = Person::read(&mut context.pool(), person_id).await?.into(); + let person: ApubPerson = Person::read(&mut context.pool(), person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)? + .into(); let community: ApubCommunity = Community::read(&mut context.pool(), community_id) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); let create_or_update = - CreateOrUpdatePage::new(post, &person, &community, kind, &context).await?; - let is_mod_action = create_or_update.object.is_mod_action(&context).await?; + CreateOrUpdatePage::new(post.into(), &person, &community, kind, &context).await?; let activity = AnnouncableActivities::CreateOrUpdatePost(create_or_update); send_activity_in_community( activity, &person, &community, ActivitySendTargets::empty(), - is_mod_action, + false, &context, ) .await?; @@ -109,30 +110,8 @@ impl ActivityHandler for CreateOrUpdatePage { let community = self.community(context).await?; verify_person_in_community(&self.actor, &community, context).await?; check_community_deleted_or_removed(&community)?; - - match self.kind { - CreateOrUpdateType::Create => { - verify_domains_match(self.actor.inner(), self.object.id.inner())?; - verify_urls_match(self.actor.inner(), self.object.creator()?.inner())?; - // Check that the post isnt locked, as that isnt possible for newly created posts. - // However, when fetching a remote post we generate a new create activity with the current - // locked value, so this check may fail. So only check if its a local community, - // because then we will definitely receive all create and update activities separately. - let is_locked = self.object.comments_enabled == Some(false); - if community.local && is_locked { - Err(LemmyErrorType::NewPostCannotBeLocked)? - } - } - CreateOrUpdateType::Update => { - let is_mod_action = self.object.is_mod_action(context).await?; - if is_mod_action { - verify_mod_action(&self.actor, &community, context).await?; - } else { - verify_domains_match(self.actor.inner(), self.object.id.inner())?; - verify_urls_match(self.actor.inner(), self.object.creator()?.inner())?; - } - } - } + verify_domains_match(self.actor.inner(), self.object.id.inner())?; + verify_urls_match(self.actor.inner(), self.object.creator()?.inner())?; ApubPost::verify(&self.object, self.actor.inner(), context).await?; Ok(()) } diff --git a/crates/apub/src/activities/create_or_update/private_message.rs b/crates/apub/src/activities/create_or_update/private_message.rs index 950f4ae99..6bba4e374 100644 --- a/crates/apub/src/activities/create_or_update/private_message.rs +++ b/crates/apub/src/activities/create_or_update/private_message.rs @@ -9,7 +9,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - protocol::verification::verify_domains_match, + protocol::verification::{verify_domains_match, verify_urls_match}, traits::{ActivityHandler, Actor, Object}, }; use lemmy_api_common::context::LemmyContext; @@ -61,6 +61,7 @@ impl ActivityHandler for CreateOrUpdateChatMessage { verify_person(&self.actor, context).await?; verify_domains_match(self.actor.inner(), self.object.id.inner())?; verify_domains_match(self.to[0].inner(), self.object.to[0].inner())?; + verify_urls_match(self.actor.inner(), self.object.attributed_to.inner())?; ApubPrivateMessage::verify(&self.object, self.actor.inner(), context).await?; Ok(()) } diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs index b12532087..c9d268e74 100644 --- a/crates/apub/src/activities/deletion/mod.rs +++ b/crates/apub/src/activities/deletion/mod.rs @@ -39,7 +39,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use std::ops::Deref; use url::Url; @@ -87,6 +87,7 @@ pub(crate) async fn send_apub_delete_private_message( let recipient_id = pm.recipient_id; let recipient: ApubPerson = Person::read(&mut context.pool(), recipient_id) .await? + .ok_or(LemmyErrorType::CouldntFindPerson)? .into(); let deletable = DeletableObjects::PrivateMessage(pm.into()); diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index 6547b957d..d81e7cabf 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -245,7 +245,9 @@ pub async fn match_outgoing_activities( CreateOrUpdatePage::send(post, creator_id, CreateOrUpdateType::Update, context).await } DeletePost(post, person, data) => { - let community = Community::read(&mut context.pool(), post.community_id).await?; + let community = Community::read(&mut context.pool(), post.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; send_apub_delete_in_community( person, community, @@ -262,7 +264,9 @@ pub async fn match_outgoing_activities( reason, removed, } => { - let community = Community::read(&mut context.pool(), post.community_id).await?; + let community = Community::read(&mut context.pool(), post.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; send_apub_delete_in_community( moderator, community, diff --git a/crates/apub/src/activities/voting/mod.rs b/crates/apub/src/activities/voting/mod.rs index 2022252c1..3e59cb7d0 100644 --- a/crates/apub/src/activities/voting/mod.rs +++ b/crates/apub/src/activities/voting/mod.rs @@ -44,7 +44,7 @@ pub(crate) async fn send_like_activity( let activity = AnnouncableActivities::Vote(vote); send_activity_in_community(activity, &actor, &community, empty, false, &context).await } else { - // Lemmy API doesnt distinguish between Undo/Like and Undo/Dislike, so we hardcode it here. + // Lemmy API doesn't distinguish between Undo/Like and Undo/Dislike, so we hardcode it here. let vote = Vote::new(object_id, &actor, &community, VoteType::Like, &context)?; let undo_vote = UndoVote::new(vote, &actor, &community, &context)?; let activity = AnnouncableActivities::UndoVote(undo_vote); diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index a231b9080..25d197007 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -58,7 +58,12 @@ pub async fn list_comments( // If a parent_id is given, fetch the comment to get the path let parent_path = if let Some(parent_id) = parent_id { - Some(Comment::read(&mut context.pool(), parent_id).await?.path) + Some( + Comment::read(&mut context.pool(), parent_id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)? + .path, + ) } else { None }; diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 87e4bc679..ec5412de8 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -23,7 +23,9 @@ pub async fn list_posts( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; check_private_instance(&local_user_view, &local_site.local_site)?; diff --git a/crates/apub/src/api/read_community.rs b/crates/apub/src/api/read_community.rs index 0d32a0b49..dae7719ae 100644 --- a/crates/apub/src/api/read_community.rs +++ b/crates/apub/src/api/read_community.rs @@ -56,8 +56,8 @@ pub async fn get_community( person_id, is_mod_or_admin, ) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id) .await diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index d4015f62d..0d65ab4f7 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -26,7 +26,9 @@ pub async fn read_person( Err(LemmyErrorType::NoIdGiven)? } - let local_site = SiteView::read_local(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; check_private_instance(&local_user_view, &local_site.local_site)?; @@ -46,7 +48,9 @@ pub async fn read_person( // You don't need to return settings for the user, since this comes back with GetSite // `my_user` - let person_view = PersonView::read(&mut context.pool(), person_details_id).await?; + let person_view = PersonView::read(&mut context.pool(), person_details_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let sort = data.sort; let page = data.page; diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index 6ab98a052..47f6c5d06 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -53,20 +53,36 @@ async fn convert_response( match object { Post(p) => { removed_or_deleted = p.deleted || p.removed; - res.post = Some(PostView::read(pool, p.id, user_id, false).await?) + res.post = Some( + PostView::read(pool, p.id, user_id, false) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?, + ) } Comment(c) => { removed_or_deleted = c.deleted || c.removed; - res.comment = Some(CommentView::read(pool, c.id, user_id).await?) + res.comment = Some( + CommentView::read(pool, c.id, user_id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?, + ) } PersonOrCommunity(p) => match *p { UserOrCommunity::User(u) => { removed_or_deleted = u.deleted; - res.person = Some(PersonView::read(pool, u.id).await?) + res.person = Some( + PersonView::read(pool, u.id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?, + ) } UserOrCommunity::Community(c) => { removed_or_deleted = c.deleted || c.removed; - res.community = Some(CommunityView::read(pool, c.id, user_id, false).await?) + res.community = Some( + CommunityView::read(pool, c.id, user_id, false) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?, + ) } }, }; diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index 0cac8351a..f3cd36faf 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -13,7 +13,7 @@ use lemmy_db_views::{ structs::{LocalUserView, SiteView}, }; use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] pub async fn search( @@ -21,7 +21,9 @@ pub async fn search( context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; check_private_instance(&local_user_view, &local_site.local_site)?; diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 8053d66a0..558551632 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -4,9 +4,10 @@ use crate::objects::{ person::ApubPerson, post::ApubPost, }; -use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; +use activitypub_federation::{config::Data, fetch::object_id::ObjectId, traits::Object}; use actix_web::web::Json; use futures::{future::try_join_all, StreamExt}; +use itertools::Itertools; use lemmy_api_common::{context::LemmyContext, SuccessResponse}; use lemmy_db_schema::{ newtypes::DbUrl, @@ -30,8 +31,11 @@ use lemmy_utils::{ spawn_try_task, }; use serde::{Deserialize, Serialize}; +use std::future::Future; use tracing::info; +const PARALLELISM: usize = 10; + /// Backup of user data. This struct should never be changed so that the data can be used as a /// long-term backup in case the instance goes down unexpectedly. All fields are optional to allow /// importing partial backups. @@ -40,7 +44,7 @@ use tracing::info; /// /// Be careful with any changes to this struct, to avoid breaking changes which could prevent /// importing older backups. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct UserSettingsBackup { pub display_name: Option, pub bio: Option, @@ -167,141 +171,91 @@ pub async fn import_settings( } spawn_try_task(async move { - const PARALLELISM: usize = 10; let person_id = local_user_view.person.id; - // These tasks fetch objects from remote instances which might be down. - // TODO: Would be nice if we could send a list of failed items with api response, but then - // the request would likely timeout. - let mut failed_items = vec![]; - info!( - "Starting settings backup for {}", + "Starting settings import for {}", local_user_view.person.name ); - futures::stream::iter( - data - .followed_communities - .clone() - .into_iter() - // reset_request_count works like clone, and is necessary to avoid running into request limit - .map(|f| (f, context.reset_request_count())) - .map(|(followed, context)| async move { - // need to reset outgoing request count to avoid running into limit - let community = followed.dereference(&context).await?; - let form = CommunityFollowerForm { - person_id, - community_id: community.id, - pending: true, - }; - CommunityFollower::follow(&mut context.pool(), &form).await?; - LemmyResult::Ok(()) - }), + let failed_followed_communities = fetch_and_import( + data.followed_communities.clone(), + &context, + |(followed, context)| async move { + let community = followed.dereference(&context).await?; + let form = CommunityFollowerForm { + person_id, + community_id: community.id, + pending: true, + }; + CommunityFollower::follow(&mut context.pool(), &form).await?; + LemmyResult::Ok(()) + }, ) - .buffer_unordered(PARALLELISM) - .collect::>() - .await - .into_iter() - .enumerate() - .for_each(|(i, r)| { - if let Err(e) = r { - failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone())); - info!("Failed to import followed community: {e}"); - } - }); - - futures::stream::iter( - data - .saved_posts - .clone() - .into_iter() - .map(|s| (s, context.reset_request_count())) - .map(|(saved, context)| async move { - let post = saved.dereference(&context).await?; - let form = PostSavedForm { - person_id, - post_id: post.id, - }; - PostSaved::save(&mut context.pool(), &form).await?; - LemmyResult::Ok(()) - }), - ) - .buffer_unordered(PARALLELISM) - .collect::>() - .await - .into_iter() - .enumerate() - .for_each(|(i, r)| { - if let Err(e) = r { - failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone())); - info!("Failed to import saved post community: {e}"); - } - }); - - futures::stream::iter( - data - .saved_comments - .clone() - .into_iter() - .map(|s| (s, context.reset_request_count())) - .map(|(saved, context)| async move { - let comment = saved.dereference(&context).await?; - let form = CommentSavedForm { - person_id, - comment_id: comment.id, - }; - CommentSaved::save(&mut context.pool(), &form).await?; - LemmyResult::Ok(()) - }), - ) - .buffer_unordered(PARALLELISM) - .collect::>() - .await - .into_iter() - .enumerate() - .for_each(|(i, r)| { - if let Err(e) = r { - failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone())); - info!("Failed to import saved comment community: {e}"); - } - }); - - let failed_items: Vec<_> = failed_items.into_iter().flatten().collect(); - info!( - "Finished settings backup for {}, failed items: {:#?}", - local_user_view.person.name, failed_items - ); - - // These tasks don't connect to any remote instances but only insert directly in the database. - // That means the only error condition are db connection failures, so no extra error handling is - // needed. - try_join_all(data.blocked_communities.iter().map(|blocked| async { - // dont fetch unknown blocked objects from home server - let community = blocked.dereference_local(&context).await?; - let form = CommunityBlockForm { - person_id, - community_id: community.id, - }; - CommunityBlock::block(&mut context.pool(), &form).await?; - LemmyResult::Ok(()) - })) .await?; - try_join_all(data.blocked_users.iter().map(|blocked| async { - // dont fetch unknown blocked objects from home server - let target = blocked.dereference_local(&context).await?; - let form = PersonBlockForm { - person_id, - target_id: target.id, - }; - PersonBlock::block(&mut context.pool(), &form).await?; - LemmyResult::Ok(()) - })) + let failed_saved_posts = fetch_and_import( + data.saved_posts.clone(), + &context, + |(saved, context)| async move { + let post = saved.dereference(&context).await?; + let form = PostSavedForm { + person_id, + post_id: post.id, + }; + PostSaved::save(&mut context.pool(), &form).await?; + LemmyResult::Ok(()) + }, + ) + .await?; + + let failed_saved_comments = fetch_and_import( + data.saved_comments.clone(), + &context, + |(saved, context)| async move { + let comment = saved.dereference(&context).await?; + let form = CommentSavedForm { + person_id, + comment_id: comment.id, + }; + CommentSaved::save(&mut context.pool(), &form).await?; + LemmyResult::Ok(()) + }, + ) + .await?; + + let failed_community_blocks = fetch_and_import( + data.blocked_communities.clone(), + &context, + |(blocked, context)| async move { + let community = blocked.dereference(&context).await?; + let form = CommunityBlockForm { + person_id, + community_id: community.id, + }; + CommunityBlock::block(&mut context.pool(), &form).await?; + LemmyResult::Ok(()) + }, + ) + .await?; + + let failed_user_blocks = fetch_and_import( + data.blocked_users.clone(), + &context, + |(blocked, context)| async move { + let context = context.reset_request_count(); + let target = blocked.dereference(&context).await?; + let form = PersonBlockForm { + person_id, + target_id: target.id, + }; + PersonBlock::block(&mut context.pool(), &form).await?; + LemmyResult::Ok(()) + }, + ) .await?; try_join_all(data.blocked_instances.iter().map(|domain| async { - // dont fetch unknown blocked objects from home server let instance = Instance::read_or_create(&mut context.pool(), domain.clone()).await?; let form = InstanceBlockForm { person_id, @@ -312,17 +266,53 @@ pub async fn import_settings( })) .await?; + info!("Settings import completed for {}, the following items failed: {failed_followed_communities}, {failed_saved_posts}, {failed_saved_comments}, {failed_community_blocks}, {failed_user_blocks}", + local_user_view.person.name); + Ok(()) }); Ok(Json(Default::default())) } +async fn fetch_and_import( + objects: Vec>, + context: &Data, + import_fn: impl FnMut((ObjectId, Data)) -> Fut, +) -> LemmyResult +where + Kind: Object + Send + 'static, + for<'de2> ::Kind: Deserialize<'de2>, + Fut: Future>, +{ + let mut failed_items = vec![]; + futures::stream::iter( + objects + .clone() + .into_iter() + // need to reset outgoing request count to avoid running into limit + .map(|s| (s, context.reset_request_count())) + .map(import_fn), + ) + .buffer_unordered(PARALLELISM) + .collect::>() + .await + .into_iter() + .enumerate() + .for_each(|(i, r): (usize, LemmyResult<()>)| { + if r.is_err() { + if let Some(object) = objects.get(i) { + failed_items.push(object.inner().clone()); + } + } + }); + Ok(failed_items.into_iter().join(",")) +} #[cfg(test)] #[allow(clippy::indexing_slicing)] mod tests { - use crate::api::user_settings_backup::{export_settings, import_settings}; + use crate::api::user_settings_backup::{export_settings, import_settings, UserSettingsBackup}; use activitypub_federation::config::Data; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ @@ -363,7 +353,11 @@ mod tests { .build(); let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?; - Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?) + Ok( + LocalUserView::read(&mut context.pool(), local_user.id) + .await? + .ok_or(LemmyErrorType::CouldntFindLocalUser)?, + ) } #[tokio::test] @@ -396,8 +390,9 @@ mod tests { // wait for background task to finish sleep(Duration::from_millis(1000)).await; - let import_user_updated = - LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?; + let import_user_updated = LocalUserView::read(&mut context.pool(), import_user.local_user.id) + .await? + .ok_or(LemmyErrorType::CouldntFindLocalUser)?; assert_eq!( export_user.person.display_name, @@ -415,6 +410,44 @@ mod tests { Ok(()) } + #[tokio::test] + #[serial] + async fn test_settings_partial_import() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + + let export_user = + create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; + + let community_form = CommunityInsertForm::builder() + .name("testcom".to_string()) + .title("testcom".to_string()) + .instance_id(export_user.person.instance_id) + .build(); + let community = Community::create(&mut context.pool(), &community_form).await?; + let follower_form = CommunityFollowerForm { + community_id: community.id, + person_id: export_user.person.id, + pending: false, + }; + CommunityFollower::follow(&mut context.pool(), &follower_form).await?; + + let backup = export_settings(export_user.clone(), context.reset_request_count()).await?; + + let import_user = create_user("charles".to_string(), None, &context).await?; + + let backup2 = UserSettingsBackup { + followed_communities: backup.followed_communities.clone(), + ..Default::default() + }; + import_settings( + actix_web::web::Json(backup2), + import_user.clone(), + context.reset_request_count(), + ) + .await?; + Ok(()) + } + #[tokio::test] #[serial] async fn disallow_large_backup() -> LemmyResult<()> { diff --git a/crates/apub/src/collections/community_featured.rs b/crates/apub/src/collections/community_featured.rs index eb19d64ea..e092693e6 100644 --- a/crates/apub/src/collections/community_featured.rs +++ b/crates/apub/src/collections/community_featured.rs @@ -72,7 +72,8 @@ impl Collection for ApubCommunityFeatured { .to_vec(); } - // process items in parallel, to avoid long delay from fetch_site_metadata() and other processing + // process items in parallel, to avoid long delay from fetch_site_metadata() and other + // processing let stickied_posts: Vec = join_all(pages.into_iter().map(|page| { async { // use separate request counter for each item, otherwise there will be problems with diff --git a/crates/apub/src/collections/community_outbox.rs b/crates/apub/src/collections/community_outbox.rs index f56708d09..8aca82d38 100644 --- a/crates/apub/src/collections/community_outbox.rs +++ b/crates/apub/src/collections/community_outbox.rs @@ -1,6 +1,6 @@ use crate::{ activity_lists::AnnouncableActivities, - objects::{community::ApubCommunity, post::ApubPost}, + objects::community::ApubCommunity, protocol::{ activities::{ community::announce::AnnounceActivity, @@ -18,12 +18,12 @@ use activitypub_federation::{ }; use futures::future::join_all; use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url}; -use lemmy_db_schema::{ - source::{person::Person, post::Post}, - traits::Crud, - utils::FETCH_LIMIT_MAX, +use lemmy_db_schema::{utils::FETCH_LIMIT_MAX, SortType}; +use lemmy_db_views::{post_view::PostQuery, structs::SiteView}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; #[derive(Clone, Debug)] @@ -38,18 +38,30 @@ impl Collection for ApubCommunityOutbox { #[tracing::instrument(skip_all)] async fn read_local(owner: &Self::Owner, data: &Data) -> LemmyResult { - let post_list: Vec = Post::list_for_community(&mut data.pool(), owner.id) + let site = SiteView::read_local(&mut data.pool()) .await? - .into_iter() - .map(Into::into) - .collect(); + .ok_or(LemmyErrorType::LocalSiteNotSetup)? + .site; + + let post_views = PostQuery { + community_id: Some(owner.id), + sort: Some(SortType::New), + limit: Some(FETCH_LIMIT_MAX), + ..Default::default() + } + .list(&site, &mut data.pool()) + .await?; + let mut ordered_items = vec![]; - for post in post_list { - let person = Person::read(&mut data.pool(), post.creator_id) - .await? - .into(); - let create = - CreateOrUpdatePage::new(post, &person, owner, CreateOrUpdateType::Create, data).await?; + for post_view in post_views { + let create = CreateOrUpdatePage::new( + post_view.post.into(), + &post_view.creator.into(), + owner, + CreateOrUpdateType::Create, + data, + ) + .await?; let announcable = AnnouncableActivities::CreateOrUpdatePost(create); let announce = AnnounceActivity::new(announcable.try_into()?, owner, data)?; ordered_items.push(announce); @@ -90,7 +102,8 @@ impl Collection for ApubCommunityOutbox { // We intentionally ignore errors here. This is because the outbox might contain posts from old // Lemmy versions, or from other software which we cant parse. In that case, we simply skip the // item and only parse the ones that work. - // process items in parallel, to avoid long delay from fetch_site_metadata() and other processing + // process items in parallel, to avoid long delay from fetch_site_metadata() and other + // processing join_all(outbox_activities.into_iter().map(|activity| { async { // Receiving announce requires at least one local community follower for anti spam purposes. diff --git a/crates/apub/src/fetcher/mod.rs b/crates/apub/src/fetcher/mod.rs index 928656924..68fc07d30 100644 --- a/crates/apub/src/fetcher/mod.rs +++ b/crates/apub/src/fetcher/mod.rs @@ -42,9 +42,12 @@ where .splitn(2, '@') .collect_tuple() .expect("invalid query"); - let actor = DbActor::read_from_name_and_domain(&mut context.pool(), name, domain).await; - if actor.is_ok() { - Ok(actor?.into()) + let actor = DbActor::read_from_name_and_domain(&mut context.pool(), name, domain) + .await + .ok() + .flatten(); + if let Some(actor) = actor { + Ok(actor.into()) } else if local_user_view.is_some() { // Fetch the actor from its home instance using webfinger let actor: ActorType = webfinger_resolve_actor(&identifier.to_lowercase(), context).await?; @@ -59,6 +62,7 @@ where Ok( DbActor::read_from_name(&mut context.pool(), &identifier, include_deleted) .await? + .ok_or(NotFound)? .into(), ) } diff --git a/crates/apub/src/fetcher/post_or_comment.rs b/crates/apub/src/fetcher/post_or_comment.rs index 9df0a04fb..083369b9d 100644 --- a/crates/apub/src/fetcher/post_or_comment.rs +++ b/crates/apub/src/fetcher/post_or_comment.rs @@ -12,7 +12,10 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + LemmyErrorType, +}; use serde::Deserialize; use url::Url; @@ -91,9 +94,15 @@ impl InCommunity for PostOrComment { PostOrComment::Comment(c) => { Post::read(&mut context.pool(), c.post_id) .await? + .ok_or(LemmyErrorType::CouldntFindPost)? .community_id } }; - Ok(Community::read(&mut context.pool(), cid).await?.into()) + Ok( + Community::read(&mut context.pool(), cid) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? + .into(), + ) } } diff --git a/crates/apub/src/http/comment.rs b/crates/apub/src/http/comment.rs index d6b3c818d..17711817e 100644 --- a/crates/apub/src/http/comment.rs +++ b/crates/apub/src/http/comment.rs @@ -15,7 +15,7 @@ use lemmy_db_schema::{ source::{comment::Comment, community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::Deserialize; #[derive(Deserialize)] @@ -31,9 +31,16 @@ pub(crate) async fn get_apub_comment( ) -> LemmyResult { let id = CommentId(info.comment_id.parse::()?); // Can't use CommentView here because it excludes deleted/removed/local-only items - let comment: ApubComment = Comment::read(&mut context.pool(), id).await?.into(); - let post = Post::read(&mut context.pool(), comment.post_id).await?; - let community = Community::read(&mut context.pool(), post.community_id).await?; + let comment: ApubComment = Comment::read(&mut context.pool(), id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)? + .into(); + let post = Post::read(&mut context.pool(), comment.post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; + let community = Community::read(&mut context.pool(), post.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; check_community_public(&community)?; if !comment.local { diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 2085ed1ac..0f6ee57cb 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -18,7 +18,7 @@ use activitypub_federation::{ use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::ApubActor}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::Deserialize; #[derive(Deserialize, Clone)] @@ -35,6 +35,7 @@ pub(crate) async fn get_apub_community_http( let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, true) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); if community.deleted || community.removed { @@ -64,8 +65,9 @@ pub(crate) async fn get_apub_community_followers( info: web::Path, context: Data, ) -> LemmyResult { - let community = - Community::read_from_name(&mut context.pool(), &info.community_name, false).await?; + let community = Community::read_from_name(&mut context.pool(), &info.community_name, false) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; check_community_public(&community)?; let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?; create_apub_response(&followers) @@ -80,6 +82,7 @@ pub(crate) async fn get_apub_community_outbox( let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); check_community_public(&community)?; let outbox = ApubCommunityOutbox::read_local(&community, &context).await?; @@ -94,6 +97,7 @@ pub(crate) async fn get_apub_community_moderators( let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); check_community_public(&community)?; let moderators = ApubCommunityModerators::read_local(&community, &context).await?; @@ -108,6 +112,7 @@ pub(crate) async fn get_apub_community_featured( let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .into(); check_community_public(&community)?; let featured = ApubCommunityFeatured::read_local(&community, &context).await?; @@ -123,7 +128,14 @@ pub(crate) mod tests { use crate::protocol::objects::{group::Group, tombstone::Tombstone}; use actix_web::body::to_bytes; use lemmy_db_schema::{ - source::{community::CommunityInsertForm, instance::Instance}, + newtypes::InstanceId, + source::{ + community::CommunityInsertForm, + instance::Instance, + local_site::{LocalSite, LocalSiteInsertForm}, + local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm}, + site::{Site, SiteInsertForm}, + }, traits::Crud, CommunityVisibility, }; @@ -137,6 +149,8 @@ pub(crate) mod tests { ) -> LemmyResult<(Instance, Community)> { let instance = Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?; + create_local_site(context, instance.id).await?; + let community_form = CommunityInsertForm::builder() .name("testcom6".to_string()) .title("nada".to_owned()) @@ -149,6 +163,28 @@ pub(crate) mod tests { Ok((instance, community)) } + /// Necessary for the community outbox fetching + async fn create_local_site( + context: &Data, + instance_id: InstanceId, + ) -> LemmyResult<()> { + // Create a local site, since this is necessary for community fetching. + let site_form = SiteInsertForm::builder() + .name("test site".to_string()) + .instance_id(instance_id) + .build(); + let site = Site::create(&mut context.pool(), &site_form).await?; + + let local_site_form = LocalSiteInsertForm::builder().site_id(site.id).build(); + let local_site = LocalSite::create(&mut context.pool(), &local_site_form).await?; + let local_site_rate_limit_form = LocalSiteRateLimitInsertForm::builder() + .local_site_id(local_site.id) + .build(); + + LocalSiteRateLimit::create(&mut context.pool(), &local_site_rate_limit_form).await?; + Ok(()) + } + async fn decode_response(res: HttpResponse) -> LemmyResult { let body = to_bytes(res.into_body()).await.unwrap(); let body = std::str::from_utf8(&body)?; @@ -159,6 +195,7 @@ pub(crate) mod tests { #[serial] async fn test_get_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; + let (instance, community) = init(false, CommunityVisibility::Public, &context).await?; // fetch invalid community let query = CommunityQuery { @@ -167,8 +204,6 @@ pub(crate) mod tests { let res = get_apub_community_http(query.into(), context.reset_request_count()).await; assert!(res.is_err()); - let (instance, community) = init(false, CommunityVisibility::Public, &context).await?; - // fetch valid community let query = CommunityQuery { community_name: community.name.clone(), diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index 86c51338a..6303dd1b0 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -20,7 +20,8 @@ use lemmy_db_schema::{ }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; -use std::ops::Deref; +use std::{ops::Deref, time::Duration}; +use tokio::time::timeout; use url::Url; mod comment; @@ -30,13 +31,22 @@ mod post; pub mod routes; pub mod site; +const INCOMING_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(9); + pub async fn shared_inbox( request: HttpRequest, body: Bytes, data: Data, ) -> LemmyResult { - receive_activity::(request, body, &data) + let receive_fut = + receive_activity::(request, body, &data); + // Set a timeout shorter than `REQWEST_TIMEOUT` for processing incoming activities. This is to + // avoid taking a long time to process an incoming activity when a required data fetch times out. + // In this case our own instance would timeout and be marked as dead by the sender. Better to + // consider the activity broken and move on. + timeout(INCOMING_ACTIVITY_TIMEOUT, receive_fut) .await + .map_err(|_| LemmyErrorType::InboxTimeout)? } /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub @@ -97,7 +107,9 @@ pub(crate) async fn get_activity( info.id ))? .into(); - let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id).await?; + let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id) + .await? + .ok_or(LemmyErrorType::CouldntFindActivity)?; let sensitive = activity.sensitive; if sensitive { diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index b34f166b0..ba2372fe8 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -14,7 +14,7 @@ use activitypub_federation::{ use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url}; use lemmy_db_schema::{source::person::Person, traits::ApubActor}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::Deserialize; #[derive(Deserialize)] @@ -32,6 +32,7 @@ pub(crate) async fn get_apub_person_http( // TODO: this needs to be able to read deleted persons, so that it can send tombstones let person: ApubPerson = Person::read_from_name(&mut context.pool(), &user_name, true) .await? + .ok_or(LemmyErrorType::CouldntFindPerson)? .into(); if !person.deleted { @@ -60,7 +61,9 @@ pub(crate) async fn get_apub_person_outbox( info: web::Path, context: Data, ) -> LemmyResult { - let person = Person::read_from_name(&mut context.pool(), &info.user_name, false).await?; + let person = Person::read_from_name(&mut context.pool(), &info.user_name, false) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let outbox_id = generate_outbox_url(&person.actor_id)?.into(); let outbox = EmptyOutbox::new(outbox_id)?; create_apub_response(&outbox) diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs index ce6612826..513cba7ea 100644 --- a/crates/apub/src/http/post.rs +++ b/crates/apub/src/http/post.rs @@ -15,7 +15,7 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::Deserialize; #[derive(Deserialize)] @@ -31,8 +31,13 @@ pub(crate) async fn get_apub_post( ) -> LemmyResult { let id = PostId(info.post_id.parse::()?); // Can't use PostView here because it excludes deleted/removed/local-only items - let post: ApubPost = Post::read(&mut context.pool(), id).await?.into(); - let community = Community::read(&mut context.pool(), post.community_id).await?; + let post: ApubPost = Post::read(&mut context.pool(), id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)? + .into(); + let community = Community::read(&mut context.pool(), post.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; check_community_public(&community)?; if !post.local { diff --git a/crates/apub/src/http/site.rs b/crates/apub/src/http/site.rs index 622c0f676..54d3c0e32 100644 --- a/crates/apub/src/http/site.rs +++ b/crates/apub/src/http/site.rs @@ -7,11 +7,15 @@ use activitypub_federation::{config::Data, traits::Object}; use actix_web::HttpResponse; use lemmy_api_common::context::LemmyContext; use lemmy_db_views::structs::SiteView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use url::Url; pub(crate) async fn get_apub_site_http(context: Data) -> LemmyResult { - let site: ApubSite = SiteView::read_local(&mut context.pool()).await?.site.into(); + let site: ApubSite = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)? + .site + .into(); let apub = site.into_json(&context).await?; create_apub_response(&apub) diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 29c9a12eb..c8960b008 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -29,7 +29,9 @@ pub(crate) mod mentions; pub mod objects; pub mod protocol; -pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 50; +/// Maximum number of outgoing HTTP requests to fetch a single object. Needs to be high enough +/// to fetch a new community with posts, moderators and featured posts. +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 @@ -78,7 +80,10 @@ impl UrlVerifier for VerifyUrlData { /// - URL not being in the blocklist (if it is active) #[tracing::instrument(skip(local_site_data))] fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> LemmyResult<()> { - let domain = apub_id.domain().expect("apud id has domain").to_string(); + let domain = apub_id + .domain() + .ok_or(LemmyErrorType::UrlWithoutDomain)? + .to_string(); if !local_site_data .local_site @@ -158,7 +163,10 @@ pub(crate) async fn check_apub_id_valid_with_strictness( is_strict: bool, context: &LemmyContext, ) -> LemmyResult<()> { - let domain = apub_id.domain().expect("apud id has domain").to_string(); + let domain = apub_id + .domain() + .ok_or(LemmyErrorType::UrlWithoutDomain)? + .to_string(); let local_instance = context .settings() .get_hostname_without_port() @@ -185,7 +193,10 @@ pub(crate) async fn check_apub_id_valid_with_strictness( .expect("local hostname is valid"); allowed_and_local.push(local_instance); - let domain = apub_id.domain().expect("apud id has domain").to_string(); + let domain = apub_id + .domain() + .ok_or(LemmyErrorType::UrlWithoutDomain)? + .to_string(); if !allowed_and_local.contains(&domain) { Err(LemmyErrorType::FederationDisabledByStrictAllowList)? } @@ -195,7 +206,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness( /// Store received activities in the database. /// -/// This ensures that the same activity doesnt get received and processed more than once, which +/// This ensures that the same activity doesn't get received and processed more than once, which /// would be a waste of resources. #[tracing::instrument(skip(data))] async fn insert_received_activity(ap_id: &Url, data: &Data) -> LemmyResult<()> { diff --git a/crates/apub/src/mentions.rs b/crates/apub/src/mentions.rs index 92b07db8e..de472bd8a 100644 --- a/crates/apub/src/mentions.rs +++ b/crates/apub/src/mentions.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ traits::Crud, utils::DbPool, }; -use lemmy_utils::{error::LemmyResult, utils::mention::scrape_text_for_mentions}; +use lemmy_utils::{error::LemmyResult, utils::mention::scrape_text_for_mentions, LemmyErrorType}; use serde::{Deserialize, Serialize}; use serde_json::Value; use url::Url; @@ -54,7 +54,10 @@ pub async fn collect_non_local_mentions( name: Some(format!( "@{}@{}", &parent_creator.name, - &parent_creator.id().domain().expect("has domain") + &parent_creator + .id() + .domain() + .ok_or(LemmyErrorType::UrlWithoutDomain)? )), kind: MentionType::Mention, }; @@ -96,12 +99,21 @@ async fn get_comment_parent_creator( comment: &Comment, ) -> LemmyResult { let parent_creator_id = if let Some(parent_comment_id) = comment.parent_comment_id() { - let parent_comment = Comment::read(pool, parent_comment_id).await?; + let parent_comment = Comment::read(pool, parent_comment_id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; parent_comment.creator_id } else { let parent_post_id = comment.post_id; - let parent_post = Post::read(pool, parent_post_id).await?; + let parent_post = Post::read(pool, parent_post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; parent_post.creator_id }; - Ok(Person::read(pool, parent_creator_id).await?.into()) + Ok( + Person::read(pool, parent_creator_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)? + .into(), + ) } diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index ce0430cc0..466094b7f 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -91,15 +91,23 @@ impl Object for ApubComment { #[tracing::instrument(skip_all)] async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; - let creator = Person::read(&mut context.pool(), creator_id).await?; + let creator = Person::read(&mut context.pool(), creator_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let post_id = self.post_id; - let post = Post::read(&mut context.pool(), post_id).await?; + let post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; let community_id = post.community_id; - let community = Community::read(&mut context.pool(), community_id).await?; + let community = Community::read(&mut context.pool(), community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let in_reply_to = if let Some(comment_id) = self.parent_comment_id() { - let parent_comment = Comment::read(&mut context.pool(), comment_id).await?; + let parent_comment = Comment::read(&mut context.pool(), comment_id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; parent_comment.ap_id.into() } else { post.ap_id.into() @@ -220,7 +228,7 @@ pub(crate) mod tests { url: &Url, context: &Data, ) -> LemmyResult<(ApubPerson, ApubCommunity, ApubPost, ApubSite)> { - // use separate counter so this doesnt affect tests + // use separate counter so this doesn't affect tests let context2 = context.reset_request_count(); let (person, site) = parse_lemmy_person(&context2).await?; let community = parse_lemmy_community(&context2).await?; diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 6638cc68d..93c2c83ae 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -28,6 +28,7 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ + sensitive::SensitiveString, source::{ activity::ActorType, actor_language::CommunityLanguage, @@ -113,7 +114,7 @@ impl Object for ApubCommunity { featured: Some(generate_featured_url(&self.actor_id)?.into()), inbox: self.inbox_url.clone().into(), outbox: generate_outbox_url(&self.actor_id)?.into(), - followers: self.followers_url.clone().into(), + followers: self.followers_url.clone().map(Into::into), endpoints: self.shared_inbox_url.clone().map(|s| Endpoints { shared_inbox: s.into(), }), @@ -164,7 +165,7 @@ impl Object for ApubCommunity { last_refreshed_at: Some(naive_now()), icon, banner, - followers_url: Some(group.followers.clone().into()), + followers_url: group.followers.clone().map(Into::into), inbox_url: Some(group.inbox.into()), shared_inbox_url: group.endpoints.map(|e| e.shared_inbox.into()), moderators_url: group.attributed_to.clone().map(Into::into), @@ -187,11 +188,9 @@ impl Object for ApubCommunity { let context_ = context.reset_request_count(); spawn_try_task(async move { group.outbox.dereference(&community_, &context_).await.ok(); - group - .followers - .dereference(&community_, &context_) - .await - .ok(); + if let Some(followers) = group.followers { + followers.dereference(&community_, &context_).await.ok(); + } if let Some(featured) = group.featured { featured.dereference(&community_, &context_).await.ok(); } @@ -215,7 +214,7 @@ impl Actor for ApubCommunity { } fn private_key_pem(&self) -> Option { - self.private_key.clone() + self.private_key.clone().map(SensitiveString::into_inner) } fn inbox(&self) -> Url { @@ -269,13 +268,15 @@ pub(crate) mod tests { pub(crate) async fn parse_lemmy_community( context: &Data, ) -> LemmyResult { - // use separate counter so this doesnt affect tests + // use separate counter so this doesn't affect tests let context2 = context.reset_request_count(); let mut json: Group = file_to_json_object("assets/lemmy/objects/group.json")?; // change these links so they dont fetch over the network json.attributed_to = None; json.outbox = CollectionId::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox")?; - json.followers = CollectionId::parse("https://enterprise.lemmy.ml/c/tenforward/not_followers")?; + json.followers = Some(CollectionId::parse( + "https://enterprise.lemmy.ml/c/tenforward/not_followers", + )?); let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward")?; ApubCommunity::verify(&json, &url, &context2).await?; diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index 145dc63c2..24bb63b4c 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -29,6 +29,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ newtypes::InstanceId, + sensitive::SensitiveString, source::{ activity::ActorType, actor_language::SiteLanguage, @@ -45,6 +46,7 @@ use lemmy_utils::{ markdown::markdown_to_html, slurs::{check_slurs, check_slurs_opt}, }, + LemmyErrorType, }; use std::ops::Deref; use tracing::debug; @@ -99,7 +101,7 @@ impl Object for ApubSite { kind: ApplicationType::Application, id: self.id().into(), name: self.name.clone(), - preferred_username: data.domain().to_string(), + preferred_username: Some(data.domain().to_string()), content: self.sidebar.as_ref().map(|d| markdown_to_html(d)), source: self.sidebar.clone().map(Source::new), summary: self.description.clone(), @@ -137,7 +139,11 @@ impl Object for ApubSite { #[tracing::instrument(skip_all)] async fn from_json(apub: Self::Kind, context: &Data) -> LemmyResult { - let domain = apub.id.inner().domain().expect("group id has domain"); + let domain = apub + .id + .inner() + .domain() + .ok_or(LemmyErrorType::UrlWithoutDomain)?; let instance = DbInstance::read_or_create(&mut context.pool(), domain.to_string()).await?; let local_site = LocalSite::read(&mut context.pool()).await.ok(); @@ -182,7 +188,7 @@ impl Actor for ApubSite { } fn private_key_pem(&self) -> Option { - self.private_key.clone() + self.private_key.clone().map(SensitiveString::into_inner) } fn inbox(&self) -> Url { @@ -210,7 +216,9 @@ pub(in crate::objects) async fn fetch_instance_actor_for_object + C Err(e) => { // Failed to fetch instance actor, its probably not a lemmy instance debug!("Failed to dereference site for {}: {}", &instance_id, e); - let domain = instance_id.domain().expect("has domain"); + let domain = instance_id + .domain() + .ok_or(LemmyErrorType::UrlWithoutDomain)?; Ok( DbInstance::read_or_create(&mut context.pool(), domain.to_string()) .await? diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 1aac170d7..61ff04622 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -30,6 +30,7 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ + sensitive::SensitiveString, source::{ activity::ActorType, local_site::LocalSite, @@ -200,7 +201,7 @@ impl Actor for ApubPerson { } fn private_key_pem(&self) -> Option { - self.private_key.clone() + self.private_key.clone().map(SensitiveString::into_inner) } fn inbox(&self) -> Url { diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 55af850e5..59f4920d3 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -25,18 +25,12 @@ use html2text::{from_read_with_decorator, render::text_renderer::TrivialDecorato use lemmy_api_common::{ context::LemmyContext, request::generate_post_link_metadata, - utils::{ - get_url_blocklist, - local_site_opt_to_slur_regex, - process_markdown_opt, - proxy_image_link_opt_apub, - }, + utils::{get_url_blocklist, local_site_opt_to_slur_regex, process_markdown_opt}, }; use lemmy_db_schema::{ source::{ community::Community, local_site::LocalSite, - moderator::{ModLockPost, ModLockPostForm}, person::Person, post::{Post, PostInsertForm, PostUpdateForm}, }, @@ -45,7 +39,8 @@ use lemmy_db_schema::{ }; use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_utils::{ - error::{LemmyError, LemmyResult}, + error::{LemmyError, LemmyErrorType, LemmyResult}, + spawn_try_task, utils::{markdown::markdown_to_html, slurs::check_slurs_opt, validation::check_url_scheme}, }; use std::ops::Deref; @@ -108,9 +103,13 @@ impl Object for ApubPost { #[tracing::instrument(skip_all)] async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; - let creator = Person::read(&mut context.pool(), creator_id).await?; + let creator = Person::read(&mut context.pool(), creator_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let community_id = self.community_id; - let community = Community::read(&mut context.pool(), community_id).await?; + let community = Community::read(&mut context.pool(), community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let language = LanguageTag::new_single(self.language_id, &mut context.pool()).await?; let attachment = self @@ -143,7 +142,6 @@ impl Object for ApubPost { source: self.body.clone().map(Source::new), attachment, image: self.thumbnail_url.clone().map(ImageObject::new), - comments_enabled: Some(!self.locked), sensitive: Some(self.nsfw), language, published: Some(self.published), @@ -161,12 +159,8 @@ impl Object for ApubPost { expected_domain: &Url, context: &Data, ) -> LemmyResult<()> { - // We can't verify the domain in case of mod action, because the mod may be on a different - // instance from the post author. - if !page.is_mod_action(context).await? { - verify_domains_match(page.id.inner(), expected_domain)?; - verify_is_remote_object(&page.id, context)?; - }; + verify_domains_match(page.id.inner(), expected_domain)?; + verify_is_remote_object(&page.id, context)?; let community = page.community(context).await?; check_apub_id_valid_with_strictness(page.id.inner(), community.local, context).await?; @@ -214,83 +208,55 @@ impl Object for ApubPost { name = name.chars().take(MAX_TITLE_LENGTH).collect(); } - // read existing, local post if any (for generating mod log) - let old_post = page.id.dereference_local(context).await; - let first_attachment = page.attachment.first(); let local_site = LocalSite::read(&mut context.pool()).await.ok(); - let form = if !page.is_mod_action(context).await? { - let url = if let Some(attachment) = first_attachment.cloned() { - Some(attachment.url()) - } else if page.kind == PageType::Video { - // we cant display videos directly, so insert a link to external video page - Some(page.id.inner().clone()) - } else { - None - }; - check_url_scheme(&url)?; - - let alt_text = first_attachment.cloned().and_then(Attachment::alt_text); - - let url = proxy_image_link_opt_apub(url, context).await?; - - 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?; - let language_id = - LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?; - - PostInsertForm::builder() - .name(name) - .url(url.map(Into::into)) - .body(body) - .alt_text(alt_text) - .creator_id(creator.id) - .community_id(community.id) - .locked(page.comments_enabled.map(|e| !e)) - .published(page.published.map(Into::into)) - .updated(page.updated.map(Into::into)) - .deleted(Some(false)) - .nsfw(page.sensitive) - .ap_id(Some(page.id.clone().into())) - .local(Some(false)) - .language_id(language_id) - .build() + let url = if let Some(attachment) = first_attachment.cloned() { + Some(attachment.url()) + } else if page.kind == PageType::Video { + // we cant display videos directly, so insert a link to external video page + Some(page.id.inner().clone()) } else { - // if is mod action, only update locked/stickied fields, nothing else - PostInsertForm::builder() - .name(name) - .creator_id(creator.id) - .community_id(community.id) - .ap_id(Some(page.id.clone().into())) - .locked(page.comments_enabled.map(|e| !e)) - .updated(page.updated.map(Into::into)) - .build() + None }; + check_url_scheme(&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?; + let language_id = + LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?; + + let form = PostInsertForm::builder() + .name(name) + .url(url.map(Into::into)) + .body(body) + .alt_text(alt_text) + .creator_id(creator.id) + .community_id(community.id) + .published(page.published.map(Into::into)) + .updated(page.updated.map(Into::into)) + .deleted(Some(false)) + .nsfw(page.sensitive) + .ap_id(Some(page.id.clone().into())) + .local(Some(false)) + .language_id(language_id) + .build(); let timestamp = page.updated.or(page.published).unwrap_or_else(naive_now); let post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?; + let post_ = post.clone(); + let context_ = context.reset_request_count(); - generate_post_link_metadata( - post.clone(), - page.image.map(|i| i.url), - |_| None, - local_site, - context.reset_request_count(), - ); - - // write mod log entry for lock - if Page::is_locked_changed(&old_post, &page.comments_enabled) { - let form = ModLockPostForm { - mod_person_id: creator.id, - post_id: post.id, - locked: Some(post.locked), - }; - ModLockPost::create(&mut context.pool(), &form).await?; - } + // Generates a post thumbnail in background task, because some sites can be very slow to + // respond. + spawn_try_task(async move { + generate_post_link_metadata(post_, None, |_| None, local_site, context_).await + }); Ok(post.into()) } diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 4600c997a..35f2fe418 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -79,10 +79,14 @@ impl Object for ApubPrivateMessage { #[tracing::instrument(skip_all)] async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; - let creator = Person::read(&mut context.pool(), creator_id).await?; + let creator = Person::read(&mut context.pool(), creator_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let recipient_id = self.recipient_id; - let recipient = Person::read(&mut context.pool(), recipient_id).await?; + let recipient = Person::read(&mut context.pool(), recipient_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let note = ChatMessage { r#type: ChatMessageType::ChatMessage, diff --git a/crates/apub/src/protocol/activities/community/announce.rs b/crates/apub/src/protocol/activities/community/announce.rs index e149e5fd1..60720365a 100644 --- a/crates/apub/src/protocol/activities/community/announce.rs +++ b/crates/apub/src/protocol/activities/community/announce.rs @@ -23,7 +23,7 @@ pub struct AnnounceActivity { } /// Use this to receive community inbox activities, and then announce them if valid. This -/// ensures that all json fields are kept, even if Lemmy doesnt understand them. +/// ensures that all json fields are kept, even if Lemmy doesn't understand them. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct RawAnnouncableActivities { pub(crate) id: Url, diff --git a/crates/apub/src/protocol/activities/community/collection_add.rs b/crates/apub/src/protocol/activities/community/collection_add.rs index 777ad8b62..0e2ab75a6 100644 --- a/crates/apub/src/protocol/activities/community/collection_add.rs +++ b/crates/apub/src/protocol/activities/community/collection_add.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::source::community::Community; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; use url::Url; @@ -35,7 +35,9 @@ pub struct CollectionAdd { impl InCommunity for CollectionAdd { async fn community(&self, context: &Data) -> LemmyResult { let (community, _) = - Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()).await?; + Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/community/collection_remove.rs b/crates/apub/src/protocol/activities/community/collection_remove.rs index afc0c24a0..51c4761ba 100644 --- a/crates/apub/src/protocol/activities/community/collection_remove.rs +++ b/crates/apub/src/protocol/activities/community/collection_remove.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::source::community::Community; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; use url::Url; @@ -35,7 +35,9 @@ pub struct CollectionRemove { impl InCommunity for CollectionRemove { async fn community(&self, context: &Data) -> LemmyResult { let (community, _) = - Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()).await?; + Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/community/lock_page.rs b/crates/apub/src/protocol/activities/community/lock_page.rs index 0a4a2ff75..c60b86cf8 100644 --- a/crates/apub/src/protocol/activities/community/lock_page.rs +++ b/crates/apub/src/protocol/activities/community/lock_page.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::Crud}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; use strum_macros::Display; use url::Url; @@ -55,7 +55,9 @@ pub struct UndoLockPage { impl InCommunity for LockPage { async fn community(&self, context: &Data) -> LemmyResult { let post = self.object.dereference(context).await?; - let community = Community::read(&mut context.pool(), post.community_id).await?; + let community = Community::read(&mut context.pool(), post.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/create_or_update/note.rs b/crates/apub/src/protocol/activities/create_or_update/note.rs index ff0728174..43ffeb291 100644 --- a/crates/apub/src/protocol/activities/create_or_update/note.rs +++ b/crates/apub/src/protocol/activities/create_or_update/note.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::Crud}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; use url::Url; @@ -36,7 +36,9 @@ pub struct CreateOrUpdateNote { impl InCommunity for CreateOrUpdateNote { async fn community(&self, context: &Data) -> LemmyResult { let post = self.object.get_parents(context).await?.0; - let community = Community::read(&mut context.pool(), post.community_id).await?; + let community = Community::read(&mut context.pool(), post.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/deletion/delete.rs b/crates/apub/src/protocol/activities/deletion/delete.rs index 3a29da069..3b9aad079 100644 --- a/crates/apub/src/protocol/activities/deletion/delete.rs +++ b/crates/apub/src/protocol/activities/deletion/delete.rs @@ -15,7 +15,7 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use url::Url; @@ -51,7 +51,9 @@ impl InCommunity for Delete { let community_id = match DeletableObjects::read_from_db(self.object.id(), context).await? { DeletableObjects::Community(c) => c.id, DeletableObjects::Comment(c) => { - let post = Post::read(&mut context.pool(), c.post_id).await?; + let post = Post::read(&mut context.pool(), c.post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; post.community_id } DeletableObjects::Post(p) => p.community_id, @@ -60,7 +62,9 @@ impl InCommunity for Delete { return Err(anyhow!("Private message is not part of community").into()) } }; - let community = Community::read(&mut context.pool(), community_id).await?; + let community = Community::read(&mut context.pool(), community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/activities/deletion/delete_user.rs b/crates/apub/src/protocol/activities/deletion/delete_user.rs index 46b070fab..70bba0718 100644 --- a/crates/apub/src/protocol/activities/deletion/delete_user.rs +++ b/crates/apub/src/protocol/activities/deletion/delete_user.rs @@ -23,6 +23,7 @@ pub struct DeleteUser { #[serde(deserialize_with = "deserialize_one_or_many", default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub(crate) cc: Vec, - /// Nonstandard field. If present, all content from the user should be deleted along with the account + /// Nonstandard field. If present, all content from the user should be deleted along with the + /// account pub(crate) remove_data: Option, } diff --git a/crates/apub/src/protocol/activities/mod.rs b/crates/apub/src/protocol/activities/mod.rs index 024f08929..ce0808ee9 100644 --- a/crates/apub/src/protocol/activities/mod.rs +++ b/crates/apub/src/protocol/activities/mod.rs @@ -96,4 +96,10 @@ mod tests { test_json::("assets/mbin/activities/flag.json")?; Ok(()) } + + #[test] + fn test_parse_wordpress_activities() -> LemmyResult<()> { + test_json::("assets/wordpress/activities/announce.json")?; + Ok(()) + } } diff --git a/crates/apub/src/protocol/objects/group.rs b/crates/apub/src/protocol/objects/group.rs index 2ed884484..a95fff262 100644 --- a/crates/apub/src/protocol/objects/group.rs +++ b/crates/apub/src/protocol/objects/group.rs @@ -45,7 +45,7 @@ pub struct Group { /// username, set at account creation and usually fixed after that pub(crate) preferred_username: String, pub(crate) inbox: Url, - pub(crate) followers: CollectionId, + pub(crate) followers: Option>, pub(crate) public_key: PublicKey, /// title diff --git a/crates/apub/src/protocol/objects/instance.rs b/crates/apub/src/protocol/objects/instance.rs index 8f6a0f368..1f21e76da 100644 --- a/crates/apub/src/protocol/objects/instance.rs +++ b/crates/apub/src/protocol/objects/instance.rs @@ -22,7 +22,7 @@ pub struct Instance { /// site name pub(crate) name: String, /// instance domain, necessary for mastodon authorized fetch - pub(crate) preferred_username: String, + pub(crate) preferred_username: Option, pub(crate) inbox: Url, /// mandatory field in activitypub, lemmy currently serves an empty outbox pub(crate) outbox: Url, diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs index efda3d7ed..a9eb74e0c 100644 --- a/crates/apub/src/protocol/objects/mod.rs +++ b/crates/apub/src/protocol/objects/mod.rs @@ -190,4 +190,29 @@ mod tests { test_json::("assets/mobilizon/objects/person.json")?; Ok(()) } + + #[test] + fn test_parse_object_discourse() -> LemmyResult<()> { + test_json::("assets/discourse/objects/group.json")?; + test_json::("assets/discourse/objects/page.json")?; + test_json::("assets/discourse/objects/person.json")?; + Ok(()) + } + + #[test] + fn test_parse_object_nodebb() -> LemmyResult<()> { + test_json::("assets/nodebb/objects/group.json")?; + test_json::("assets/nodebb/objects/page.json")?; + test_json::("assets/nodebb/objects/person.json")?; + Ok(()) + } + + #[test] + fn test_parse_object_wordpress() -> LemmyResult<()> { + test_json::("assets/wordpress/objects/group.json")?; + test_json::("assets/wordpress/objects/page.json")?; + test_json::("assets/wordpress/objects/person.json")?; + test_json::("assets/wordpress/objects/note.json")?; + Ok(()) + } } diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs index a092cec9f..b0ae00037 100644 --- a/crates/apub/src/protocol/objects/note.rs +++ b/crates/apub/src/protocol/objects/note.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use std::ops::Deref; @@ -64,7 +64,9 @@ impl Note { PostOrComment::Post(p) => Ok((p.clone(), None)), PostOrComment::Comment(c) => { let post_id = c.post_id; - let post = Post::read(&mut context.pool(), post_id).await?; + let post = Post::read(&mut context.pool(), post_id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; Ok((post.into(), Some(c.clone()))) } } @@ -75,7 +77,9 @@ impl Note { impl InCommunity for Note { async fn community(&self, context: &Data) -> LemmyResult { let (post, _) = self.get_parents(context).await?; - let community = Community::read(&mut context.pool(), post.community_id).await?; + let community = Community::read(&mut context.pool(), post.community_id) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs index acfd8f5fd..6075b14a1 100644 --- a/crates/apub/src/protocol/objects/page.rs +++ b/crates/apub/src/protocol/objects/page.rs @@ -42,7 +42,7 @@ pub struct Page { pub(crate) kind: PageType, pub(crate) id: ObjectId, pub(crate) attributed_to: AttributedTo, - #[serde(deserialize_with = "deserialize_one_or_many")] + #[serde(deserialize_with = "deserialize_one_or_many", default)] pub(crate) to: Vec, // If there is inReplyTo field this is actually a comment and must not be parsed #[serde(deserialize_with = "deserialize_not_present", default)] @@ -60,7 +60,6 @@ pub struct Page { #[serde(default)] pub(crate) attachment: Vec, pub(crate) image: Option, - pub(crate) comments_enabled: Option, pub(crate) sensitive: Option, pub(crate) published: Option>, pub(crate) updated: Option>, @@ -156,28 +155,6 @@ pub enum HashtagType { } impl Page { - /// Only mods can change the post's locked status. So if it is changed from the default value, - /// it is a mod action and needs to be verified as such. - /// - /// Locked needs to be false on a newly created post (verified in [[CreatePost]]. - pub(crate) async fn is_mod_action(&self, context: &Data) -> LemmyResult { - let old_post = self.id.clone().dereference_local(context).await; - Ok(Page::is_locked_changed(&old_post, &self.comments_enabled)) - } - - pub(crate) fn is_locked_changed( - old_post: &Result, - new_comments_enabled: &Option, - ) -> bool { - if let Some(new_comments_enabled) = new_comments_enabled { - if let Ok(old_post) = old_post { - return new_comments_enabled != &!old_post.locked; - } - } - - false - } - pub(crate) fn creator(&self) -> LemmyResult> { match &self.attributed_to { AttributedTo::Lemmy(l) => Ok(l.clone()), @@ -233,6 +210,10 @@ impl ActivityHandler for Page { #[async_trait::async_trait] impl InCommunity for Page { async fn community(&self, context: &Data) -> LemmyResult { + if let Some(audience) = &self.audience { + return audience.dereference(context).await; + } + let community = match &self.attributed_to { AttributedTo::Lemmy(_) => { let mut iter = self.to.iter().merge(self.cc.iter()); @@ -243,7 +224,7 @@ impl InCommunity for Page { break c; } } else { - Err(LemmyErrorType::NoCommunityFoundInCc)? + Err(LemmyErrorType::CouldntFindCommunity)?; } } } @@ -251,11 +232,12 @@ impl InCommunity for Page { p.iter() .find(|a| a.kind == PersonOrGroupType::Group) .map(|a| ObjectId::::from(a.id.clone().into_inner())) - .ok_or(LemmyErrorType::PageDoesNotSpecifyGroup)? + .ok_or(LemmyErrorType::CouldntFindCommunity)? .dereference(context) .await? } }; + if let Some(audience) = &self.audience { verify_community_matches(audience, community.actor_id.clone())?; } diff --git a/crates/db_perf/Cargo.toml b/crates/db_perf/Cargo.toml index ebadde00b..44ea8a36f 100644 --- a/crates/db_perf/Cargo.toml +++ b/crates/db_perf/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "lemmy_db_perf" +publish = false version.workspace = true edition.workspace = true description.workspace = true diff --git a/crates/db_perf/src/main.rs b/crates/db_perf/src/main.rs index 6139fc18e..9092d7514 100644 --- a/crates/db_perf/src/main.rs +++ b/crates/db_perf/src/main.rs @@ -132,7 +132,8 @@ async fn try_main() -> LemmyResult<()> { // Make sure the println above shows the correct amount assert_eq!(num_inserted_posts, num_posts as usize); - // Manually trigger and wait for a statistics update to ensure consistent and high amount of accuracy in the statistics used for query planning + // Manually trigger and wait for a statistics update to ensure consistent and high amount of + // accuracy in the statistics used for query planning println!("🧮 updating database statistics"); conn.batch_execute("ANALYZE;").await?; diff --git a/crates/db_perf/src/series.rs b/crates/db_perf/src/series.rs index 0dcddbd9f..b504efc54 100644 --- a/crates/db_perf/src/series.rs +++ b/crates/db_perf/src/series.rs @@ -14,10 +14,12 @@ use diesel::{ /// Gererates a series of rows for insertion. /// -/// An inclusive range is created from `start` and `stop`. A row for each number is generated using `selection`, which can be a tuple. -/// [`current_value`] is an expression that gets the current value. +/// An inclusive range is created from `start` and `stop`. A row for each number is generated using +/// `selection`, which can be a tuple. [`current_value`] is an expression that gets the current +/// value. /// -/// For example, if there's a `numbers` table with a `number` column, this inserts all numbers from 1 to 10 in a single statement: +/// For example, if there's a `numbers` table with a `number` column, this inserts all numbers from +/// 1 to 10 in a single statement: /// /// ``` /// dsl::insert_into(numbers::table) diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index a0654f063..cc6b7d8f6 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -70,7 +70,7 @@ diesel_ltree = { workspace = true, optional = true } typed-builder = { workspace = true } async-trait = { workspace = true } tracing = { workspace = true } -deadpool = { version = "0.10.0", features = ["rt_tokio_1"], optional = true } +deadpool = { version = "0.12.1", features = ["rt_tokio_1"], optional = true } ts-rs = { workspace = true, optional = true } futures-util = { workspace = true } tokio = { workspace = true, optional = true } diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql new file mode 100644 index 000000000..fa5b01018 --- /dev/null +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -0,0 +1,570 @@ +-- A trigger is associated with a table instead of a schema, so they can't be in the `r` schema. This is +-- okay if the function specified after `EXECUTE FUNCTION` is in `r`, since dropping the function drops the trigger. +-- +-- Tables that are updated by triggers should not have foreign keys that aren't set to `INITIALLY DEFERRED` +-- (even if only other columns are updated) because triggers can run after the deletion of referenced rows and +-- before the automatic deletion of the row that references it. This is not a problem for insert or delete. +-- +-- After a row update begins, a concurrent update on the same row can't begin until the whole +-- transaction that contains the first update is finished. To reduce this locking, statements in +-- triggers should be ordered based on the likelihood of concurrent writers. For example, updating +-- site_aggregates should be done last because the same row is updated for all local stuff. If +-- it were not last, then the locking period for concurrent writers would extend to include the +-- time consumed by statements that come after. +-- +-- +-- Create triggers for both post and comments +CREATE FUNCTION r.creator_id_from_post_aggregates (agg post_aggregates) + RETURNS int IMMUTABLE PARALLEL SAFE RETURN agg.creator_id; + +CREATE FUNCTION r.creator_id_from_comment_aggregates (agg comment_aggregates) + RETURNS int IMMUTABLE PARALLEL SAFE RETURN ( + SELECT + creator_id + FROM + comment + WHERE + comment.id = agg.comment_id LIMIT 1 +); + +CREATE PROCEDURE r.post_or_comment (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ + -- When a thing gets a vote, update its aggregates and its creator's aggregates + CALL r.create_triggers ('thing_like', $$ + BEGIN + WITH thing_diff AS ( UPDATE + thing_aggregates AS a + SET + score = a.score + diff.upvotes - diff.downvotes, upvotes = a.upvotes + diff.upvotes, downvotes = a.downvotes + diff.downvotes, controversy_rank = r.controversy_rank ((a.upvotes + diff.upvotes)::numeric, (a.downvotes + diff.downvotes)::numeric) + FROM ( + SELECT + (thing_like).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (thing_like).thing_id) AS diff + WHERE + a.thing_id = diff.thing_id + AND (diff.upvotes, diff.downvotes) != (0, 0) + RETURNING + r.creator_id_from_thing_aggregates (a.*) AS creator_id, diff.upvotes - diff.downvotes AS score) + UPDATE + person_aggregates AS a + SET + thing_score = a.thing_score + diff.score FROM ( + SELECT + creator_id, sum(score) AS score FROM thing_diff GROUP BY creator_id) AS diff + WHERE + a.person_id = diff.creator_id + AND diff.score != 0; + RETURN NULL; + END; + $$); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.post_or_comment ('post'); + +CALL r.post_or_comment ('comment'); + +-- Create triggers that update counts in parent aggregates +CREATE FUNCTION r.parent_comment_ids (path ltree) + RETURNS SETOF int + LANGUAGE sql + IMMUTABLE parallel safe +BEGIN + ATOMIC + SELECT + comment_id::int + FROM + string_to_table (ltree2text (path), '.') AS comment_id + -- Skip first and last +LIMIT (nlevel (path) - 2) OFFSET 1; + +END; + +CALL r.create_triggers ('comment', $$ +BEGIN + UPDATE + person_aggregates AS a + SET + comment_count = a.comment_count + diff.comment_count + FROM ( + SELECT + (comment).creator_id, coalesce(sum(count_diff), 0) AS comment_count + FROM select_old_and_new_rows AS old_and_new_rows + WHERE + r.is_counted (comment) + GROUP BY (comment).creator_id) AS diff +WHERE + a.person_id = diff.creator_id + AND diff.comment_count != 0; + +UPDATE + comment_aggregates AS a +SET + child_count = a.child_count + diff.child_count +FROM ( + SELECT + parent_id, + coalesce(sum(count_diff), 0) AS child_count + FROM ( + -- For each inserted or deleted comment, this outputs 1 row for each parent comment. + -- For example, this: + -- + -- count_diff | (comment).path + -- ------------+---------------- + -- 1 | 0.5.6.7 + -- 1 | 0.5.6.7.8 + -- + -- becomes this: + -- + -- count_diff | parent_id + -- ------------+----------- + -- 1 | 5 + -- 1 | 6 + -- 1 | 5 + -- 1 | 6 + -- 1 | 7 + SELECT + count_diff, + parent_id + FROM + select_old_and_new_rows AS old_and_new_rows, + LATERAL r.parent_comment_ids ((comment).path) AS parent_id) AS expanded_old_and_new_rows + GROUP BY + parent_id) AS diff +WHERE + a.comment_id = diff.parent_id + AND diff.child_count != 0; + +WITH post_diff AS ( + UPDATE + post_aggregates AS a + SET + comments = a.comments + diff.comments, + newest_comment_time = GREATEST (a.newest_comment_time, diff.newest_comment_time), + newest_comment_time_necro = GREATEST (a.newest_comment_time_necro, diff.newest_comment_time_necro) + FROM ( + SELECT + post.id AS post_id, + coalesce(sum(count_diff), 0) AS comments, + -- Old rows are excluded using `count_diff = 1` + max((comment).published) FILTER (WHERE count_diff = 1) AS newest_comment_time, + max((comment).published) FILTER (WHERE count_diff = 1 + -- Ignore comments from the post's creator + AND post.creator_id != (comment).creator_id + -- Ignore comments on old posts + AND post.published > ((comment).published - '2 days'::interval)) AS newest_comment_time_necro, + r.is_counted (post.*) AS include_in_community_aggregates + FROM + select_old_and_new_rows AS old_and_new_rows + LEFT JOIN post ON post.id = (comment).post_id + WHERE + r.is_counted (comment) + GROUP BY + post.id) AS diff + WHERE + a.post_id = diff.post_id + AND (diff.comments, + GREATEST (a.newest_comment_time, diff.newest_comment_time), + GREATEST (a.newest_comment_time_necro, diff.newest_comment_time_necro)) != (0, + a.newest_comment_time, + a.newest_comment_time_necro) + RETURNING + a.community_id, + diff.comments, + diff.include_in_community_aggregates) +UPDATE + community_aggregates AS a +SET + comments = a.comments + diff.comments +FROM ( + SELECT + community_id, + sum(comments) AS comments + FROM + post_diff + WHERE + post_diff.include_in_community_aggregates + GROUP BY + community_id) AS diff +WHERE + a.community_id = diff.community_id + AND diff.comments != 0; + +UPDATE + site_aggregates AS a +SET + comments = a.comments + diff.comments +FROM ( + SELECT + coalesce(sum(count_diff), 0) AS comments + FROM + select_old_and_new_rows AS old_and_new_rows + WHERE + r.is_counted (comment) + AND (comment).local) AS diff +WHERE + diff.comments != 0; + +RETURN NULL; + +END; + +$$); + +CALL r.create_triggers ('post', $$ +BEGIN + UPDATE + person_aggregates AS a + SET + post_count = a.post_count + diff.post_count + FROM ( + SELECT + (post).creator_id, coalesce(sum(count_diff), 0) AS post_count + FROM select_old_and_new_rows AS old_and_new_rows + WHERE + r.is_counted (post) + GROUP BY (post).creator_id) AS diff +WHERE + a.person_id = diff.creator_id + AND diff.post_count != 0; + +UPDATE + community_aggregates AS a +SET + posts = a.posts + diff.posts +FROM ( + SELECT + (post).community_id, + coalesce(sum(count_diff), 0) AS posts + FROM + select_old_and_new_rows AS old_and_new_rows + WHERE + r.is_counted (post) + GROUP BY + (post).community_id) AS diff +WHERE + a.community_id = diff.community_id + AND diff.posts != 0; + +UPDATE + site_aggregates AS a +SET + posts = a.posts + diff.posts +FROM ( + SELECT + coalesce(sum(count_diff), 0) AS posts + FROM + select_old_and_new_rows AS old_and_new_rows + WHERE + r.is_counted (post) + AND (post).local) AS diff +WHERE + diff.posts != 0; + +RETURN NULL; + +END; + +$$); + +CALL r.create_triggers ('community', $$ +BEGIN + UPDATE + site_aggregates AS a + SET + communities = a.communities + diff.communities + FROM ( + SELECT + coalesce(sum(count_diff), 0) AS communities + FROM select_old_and_new_rows AS old_and_new_rows + WHERE + r.is_counted (community) + AND (community).local) AS diff +WHERE + diff.communities != 0; + +RETURN NULL; + +END; + +$$); + +CALL r.create_triggers ('person', $$ +BEGIN + UPDATE + site_aggregates AS a + SET + users = a.users + diff.users + FROM ( + SELECT + coalesce(sum(count_diff), 0) AS users + FROM select_old_and_new_rows AS old_and_new_rows + WHERE (person).local) AS diff +WHERE + diff.users != 0; + +RETURN NULL; + +END; + +$$); + +-- For community_aggregates.comments, don't include comments of deleted or removed posts +CREATE FUNCTION r.update_comment_count_from_post () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + community_aggregates AS a + SET + comments = a.comments + diff.comments + FROM ( + SELECT + old_post.community_id, + sum(( + CASE WHEN r.is_counted (new_post.*) THEN + 1 + ELSE + -1 + END) * post_aggregates.comments) AS comments + FROM + new_post + INNER JOIN old_post ON new_post.id = old_post.id + AND (r.is_counted (new_post.*) != r.is_counted (old_post.*)) + INNER JOIN post_aggregates ON post_aggregates.post_id = new_post.id + GROUP BY + old_post.community_id) AS diff +WHERE + a.community_id = diff.community_id + AND diff.comments != 0; + RETURN NULL; +END; +$$; + +CREATE TRIGGER comment_count + AFTER UPDATE ON post REFERENCING OLD TABLE AS old_post NEW TABLE AS new_post + FOR EACH STATEMENT + EXECUTE FUNCTION r.update_comment_count_from_post (); + +-- Count subscribers for communities. +-- subscribers should be updated only when a local community is followed by a local or remote person. +-- subscribers_local should be updated only when a local person follows a local or remote community. +CALL r.create_triggers ('community_follower', $$ +BEGIN + UPDATE + community_aggregates AS a + SET + subscribers = a.subscribers + diff.subscribers, subscribers_local = a.subscribers_local + diff.subscribers_local + FROM ( + SELECT + (community_follower).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local + FROM select_old_and_new_rows AS old_and_new_rows + LEFT JOIN community ON community.id = (community_follower).community_id + LEFT JOIN person ON person.id = (community_follower).person_id GROUP BY (community_follower).community_id) AS diff +WHERE + a.community_id = diff.community_id + AND (diff.subscribers, diff.subscribers_local) != (0, 0); + +RETURN NULL; + +END; + +$$); + +-- These triggers create and update rows in each aggregates table to match its associated table's rows. +-- Deleting rows and updating IDs are already handled by `CASCADE` in foreign key constraints. +CREATE FUNCTION r.comment_aggregates_from_comment () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO comment_aggregates (comment_id, published) + SELECT + id, + published + FROM + new_comment; + RETURN NULL; +END; +$$; + +CREATE TRIGGER aggregates + AFTER INSERT ON comment REFERENCING NEW TABLE AS new_comment + FOR EACH STATEMENT + EXECUTE FUNCTION r.comment_aggregates_from_comment (); + +CREATE FUNCTION r.community_aggregates_from_community () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO community_aggregates (community_id, published) + SELECT + id, + published + FROM + new_community; + RETURN NULL; +END; +$$; + +CREATE TRIGGER aggregates + AFTER INSERT ON community REFERENCING NEW TABLE AS new_community + FOR EACH STATEMENT + EXECUTE FUNCTION r.community_aggregates_from_community (); + +CREATE FUNCTION r.person_aggregates_from_person () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO person_aggregates (person_id) + SELECT + id + FROM + new_person; + RETURN NULL; +END; +$$; + +CREATE TRIGGER aggregates + AFTER INSERT ON person REFERENCING NEW TABLE AS new_person + FOR EACH STATEMENT + EXECUTE FUNCTION r.person_aggregates_from_person (); + +CREATE FUNCTION r.post_aggregates_from_post () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO post_aggregates (post_id, published, newest_comment_time, newest_comment_time_necro, community_id, creator_id, instance_id, featured_community, featured_local) + SELECT + new_post.id, + new_post.published, + new_post.published, + new_post.published, + new_post.community_id, + new_post.creator_id, + community.instance_id, + new_post.featured_community, + new_post.featured_local + FROM + new_post + INNER JOIN community ON community.id = new_post.community_id; + RETURN NULL; +END; +$$; + +CREATE TRIGGER aggregates + AFTER INSERT ON post REFERENCING NEW TABLE AS new_post + FOR EACH STATEMENT + EXECUTE FUNCTION r.post_aggregates_from_post (); + +CREATE FUNCTION r.post_aggregates_from_post_update () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + post_aggregates + SET + featured_community = new_post.featured_community, + featured_local = new_post.featured_local + FROM + new_post + INNER JOIN old_post ON old_post.id = new_post.id + AND (old_post.featured_community, + old_post.featured_local) != (new_post.featured_community, + old_post.featured_local) + WHERE + post_aggregates.post_id = new_post.id; + RETURN NULL; +END; +$$; + +CREATE TRIGGER aggregates_update + AFTER UPDATE ON post REFERENCING OLD TABLE AS old_post NEW TABLE AS new_post + FOR EACH STATEMENT + EXECUTE FUNCTION r.post_aggregates_from_post_update (); + +CREATE FUNCTION r.site_aggregates_from_site () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + -- only 1 row can be in site_aggregates because of the index idx_site_aggregates_1_row_only. + -- we only ever want to have a single value in site_aggregate because the site_aggregate triggers update all rows in that table. + -- a cleaner check would be to insert it for the local_site but that would break assumptions at least in the tests + INSERT INTO site_aggregates (site_id) + VALUES (NEW.id) + ON CONFLICT ((TRUE)) + DO NOTHING; + RETURN NULL; +END; +$$; + +CREATE TRIGGER aggregates + AFTER INSERT ON site + FOR EACH ROW + EXECUTE FUNCTION r.site_aggregates_from_site (); + +-- Change the order of some cascading deletions to make deletion triggers run before the deletion of rows that the triggers need to read +CREATE FUNCTION r.delete_comments_before_post () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + DELETE FROM comment AS c + WHERE c.post_id = OLD.id; + RETURN OLD; +END; +$$; + +CREATE TRIGGER delete_comments + BEFORE DELETE ON post + FOR EACH ROW + EXECUTE FUNCTION r.delete_comments_before_post (); + +CREATE FUNCTION r.delete_follow_before_person () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + DELETE FROM community_follower AS c + WHERE c.person_id = OLD.id; + RETURN OLD; +END; +$$; + +CREATE TRIGGER delete_follow + BEFORE DELETE ON person + FOR EACH ROW + EXECUTE FUNCTION r.delete_follow_before_person (); + +-- Triggers that change values before insert or update +CREATE FUNCTION r.comment_change_values () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +DECLARE + id text = NEW.id::text; +BEGIN + -- Make `path` end with `id` if it doesn't already + IF NOT (NEW.path ~ ('*.' || id)::lquery) THEN + NEW.path = NEW.path || id; + END IF; + RETURN NEW; +END +$$; + +CREATE TRIGGER change_values + BEFORE INSERT OR UPDATE ON comment + FOR EACH ROW + EXECUTE FUNCTION r.comment_change_values (); + diff --git a/crates/db_schema/replaceable_schema/utils.sql b/crates/db_schema/replaceable_schema/utils.sql new file mode 100644 index 000000000..f236c5387 --- /dev/null +++ b/crates/db_schema/replaceable_schema/utils.sql @@ -0,0 +1,146 @@ +-- Each calculation used in triggers should be a single SQL language +-- expression so it can be inlined in migrations. +CREATE FUNCTION r.controversy_rank (upvotes numeric, downvotes numeric) + RETURNS float + LANGUAGE sql + IMMUTABLE PARALLEL SAFE RETURN 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; + +CREATE FUNCTION r.hot_rank (score numeric, published timestamp with time zone) + RETURNS double precision + LANGUAGE sql + IMMUTABLE PARALLEL SAFE RETURN + -- after a week, it will default to 0. + CASE WHEN ( +now() - published) > '0 days' + AND ( +now() - published) < '7 days' THEN + -- Use greatest(2,score), so that the hot_rank will be positive and not ignored. + log ( + greatest (2, score + 2)) / power (((EXTRACT(EPOCH FROM (now() - published)) / 3600) + 2), 1.8) + ELSE + -- if the post is from the future, set hot score to 0. otherwise you can game the post to + -- always be on top even with only 1 vote by setting it to the future + 0.0 + END; + +CREATE FUNCTION r.scaled_rank (score numeric, published timestamp with time zone, users_active_month numeric) + RETURNS double precision + LANGUAGE sql + IMMUTABLE PARALLEL SAFE + -- Add 2 to avoid divide by zero errors + -- Default for score = 1, active users = 1, and now, is (0.1728 / log(2 + 1)) = 0.3621 + -- There may need to be a scale factor multiplied to users_active_month, to make + -- the log curve less pronounced. This can be tuned in the future. + RETURN ( + r.hot_rank (score, published) / log(2 + users_active_month) +); + +-- For tables with `deleted` and `removed` columns, this function determines which rows to include in a count. +CREATE FUNCTION r.is_counted (item record) + RETURNS bool + LANGUAGE plpgsql + IMMUTABLE PARALLEL SAFE + AS $$ +BEGIN + RETURN COALESCE(NOT (item.deleted + OR item.removed), FALSE); +END; +$$; + +-- 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. +-- * Transition tables must be relevant for the operation type (for example, `NEW TABLE` is +-- not allowed for a `DELETE` trigger) +-- * Transition tables are only provided to the trigger function, not to functions that it calls. +-- +-- This function can only be called once per table. The trigger function body given as the 2nd argument +-- and can contain these names, which are replaced with a `SELECT` statement in parenthesis if needed: +-- * `select_old_rows` +-- * `select_new_rows` +-- * `select_old_and_new_rows` with 2 columns: +-- 1. `count_diff`: `-1` for old rows and `1` for new rows, which can be used with `sum` to get the number +-- to add to a count +-- 2. (same name as the trigger's table): the old or new row as a composite value +CREATE PROCEDURE r.create_triggers (table_name text, function_body text) +LANGUAGE plpgsql +AS $a$ +DECLARE + defs text := $$ + -- Delete + CREATE FUNCTION r.thing_delete_statement () + RETURNS TRIGGER + LANGUAGE plpgsql + AS function_body_delete; + CREATE TRIGGER delete_statement + AFTER DELETE ON thing REFERENCING OLD TABLE AS select_old_rows + FOR EACH STATEMENT + EXECUTE FUNCTION r.thing_delete_statement ( ); + -- Insert + CREATE FUNCTION r.thing_insert_statement ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS function_body_insert; + CREATE TRIGGER insert_statement + AFTER INSERT ON thing REFERENCING NEW TABLE AS select_new_rows + FOR EACH STATEMENT + EXECUTE FUNCTION r.thing_insert_statement ( ); + -- Update + CREATE FUNCTION r.thing_update_statement ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS function_body_update; + CREATE TRIGGER update_statement + AFTER UPDATE ON thing REFERENCING OLD TABLE AS select_old_rows NEW TABLE AS select_new_rows + FOR EACH STATEMENT + EXECUTE FUNCTION r.thing_update_statement ( ); + $$; + select_old_and_new_rows text := $$ ( + SELECT + -1 AS count_diff, + old_table::thing AS thing + FROM + select_old_rows AS old_table + UNION ALL + SELECT + 1 AS count_diff, + new_table::thing AS thing + FROM + select_new_rows AS new_table) $$; + empty_select_new_rows text := $$ ( + SELECT + * + FROM + -- Real transition table + select_old_rows + WHERE + FALSE) $$; + empty_select_old_rows text := $$ ( + SELECT + * + FROM + -- Real transition table + select_new_rows + WHERE + FALSE) $$; + BEGIN + function_body := replace(function_body, 'select_old_and_new_rows', select_old_and_new_rows); + -- `select_old_rows` and `select_new_rows` are made available as empty tables if they don't already exist + defs := replace(defs, 'function_body_delete', quote_literal(replace(function_body, 'select_new_rows', empty_select_new_rows))); + defs := replace(defs, 'function_body_insert', quote_literal(replace(function_body, 'select_old_rows', empty_select_old_rows))); + defs := replace(defs, 'function_body_update', quote_literal(function_body)); + defs := replace(defs, 'thing', table_name); + EXECUTE defs; +END; +$a$; + diff --git a/crates/db_schema/src/aggregates/comment_aggregates.rs b/crates/db_schema/src/aggregates/comment_aggregates.rs index 2120c7f38..915d17b1d 100644 --- a/crates/db_schema/src/aggregates/comment_aggregates.rs +++ b/crates/db_schema/src/aggregates/comment_aggregates.rs @@ -1,5 +1,6 @@ use crate::{ aggregates::structs::CommentAggregates, + diesel::OptionalExtension, newtypes::CommentId, schema::comment_aggregates, utils::{functions::hot_rank, get_conn, DbPool}, @@ -8,12 +9,13 @@ use diesel::{result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; impl CommentAggregates { - pub async fn read(pool: &mut DbPool<'_>, comment_id: CommentId) -> Result { + pub async fn read(pool: &mut DbPool<'_>, comment_id: CommentId) -> Result, Error> { let conn = &mut get_conn(pool).await?; comment_aggregates::table .find(comment_id) - .first::(conn) + .first(conn) .await + .optional() } pub async fn update_hot_rank( @@ -125,6 +127,7 @@ mod tests { let comment_aggs_before_delete = CommentAggregates::read(pool, inserted_comment.id) .await + .unwrap() .unwrap(); assert_eq!(1, comment_aggs_before_delete.score); @@ -143,6 +146,7 @@ mod tests { let comment_aggs_after_dislike = CommentAggregates::read(pool, inserted_comment.id) .await + .unwrap() .unwrap(); assert_eq!(0, comment_aggs_after_dislike.score); @@ -155,6 +159,7 @@ mod tests { .unwrap(); let after_like_remove = CommentAggregates::read(pool, inserted_comment.id) .await + .unwrap() .unwrap(); assert_eq!(-1, after_like_remove.score); assert_eq!(0, after_like_remove.upvotes); @@ -164,8 +169,10 @@ mod tests { Post::delete(pool, inserted_post.id).await.unwrap(); // Should be none found, since the post was deleted - let after_delete = CommentAggregates::read(pool, inserted_comment.id).await; - assert!(after_delete.is_err()); + let after_delete = CommentAggregates::read(pool, inserted_comment.id) + .await + .unwrap(); + assert!(after_delete.is_none()); // This should delete all the associated rows, and fire triggers Person::delete(pool, another_inserted_person.id) diff --git a/crates/db_schema/src/aggregates/community_aggregates.rs b/crates/db_schema/src/aggregates/community_aggregates.rs index f1f54663b..0cf63809d 100644 --- a/crates/db_schema/src/aggregates/community_aggregates.rs +++ b/crates/db_schema/src/aggregates/community_aggregates.rs @@ -1,5 +1,6 @@ use crate::{ aggregates::structs::CommunityAggregates, + diesel::OptionalExtension, newtypes::CommunityId, schema::{community_aggregates, community_aggregates::subscribers}, utils::{get_conn, DbPool}, @@ -8,12 +9,16 @@ use diesel::{result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; impl CommunityAggregates { - pub async fn read(pool: &mut DbPool<'_>, for_community_id: CommunityId) -> Result { + pub async fn read( + pool: &mut DbPool<'_>, + for_community_id: CommunityId, + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; community_aggregates::table .find(for_community_id) - .first::(conn) + .first(conn) .await + .optional() } pub async fn update_federated_followers( @@ -25,7 +30,7 @@ impl CommunityAggregates { let new_subscribers: i64 = new_subscribers.into(); diesel::update(community_aggregates::table.find(for_community_id)) .set(subscribers.eq(new_subscribers)) - .get_result::(conn) + .get_result(conn) .await } } @@ -153,6 +158,7 @@ mod tests { let community_aggregates_before_delete = CommunityAggregates::read(pool, inserted_community.id) .await + .unwrap() .unwrap(); assert_eq!(2, community_aggregates_before_delete.subscribers); @@ -163,6 +169,7 @@ mod tests { // Test the other community let another_community_aggs = CommunityAggregates::read(pool, another_inserted_community.id) .await + .unwrap() .unwrap(); assert_eq!(1, another_community_aggs.subscribers); assert_eq!(1, another_community_aggs.subscribers_local); @@ -175,6 +182,7 @@ mod tests { .unwrap(); let after_unfollow = CommunityAggregates::read(pool, inserted_community.id) .await + .unwrap() .unwrap(); assert_eq!(1, after_unfollow.subscribers); assert_eq!(1, after_unfollow.subscribers_local); @@ -185,6 +193,7 @@ mod tests { .unwrap(); let after_follow_again = CommunityAggregates::read(pool, inserted_community.id) .await + .unwrap() .unwrap(); assert_eq!(2, after_follow_again.subscribers); assert_eq!(2, after_follow_again.subscribers_local); @@ -193,6 +202,7 @@ mod tests { Post::delete(pool, inserted_post.id).await.unwrap(); let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id) .await + .unwrap() .unwrap(); assert_eq!(0, after_parent_post_delete.comments); assert_eq!(0, after_parent_post_delete.posts); @@ -203,6 +213,7 @@ mod tests { .unwrap(); let after_person_delete = CommunityAggregates::read(pool, inserted_community.id) .await + .unwrap() .unwrap(); assert_eq!(1, after_person_delete.subscribers); assert_eq!(1, after_person_delete.subscribers_local); @@ -223,7 +234,9 @@ mod tests { assert_eq!(1, another_community_num_deleted); // Should be none found, since the creator was deleted - let after_delete = CommunityAggregates::read(pool, inserted_community.id).await; - assert!(after_delete.is_err()); + let after_delete = CommunityAggregates::read(pool, inserted_community.id) + .await + .unwrap(); + assert!(after_delete.is_none()); } } diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index 520b45225..03295173f 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -1,3 +1,4 @@ +pub(crate) use crate::diesel::OptionalExtension; use crate::{ aggregates::structs::PersonAggregates, newtypes::PersonId, @@ -8,12 +9,13 @@ use diesel::{result::Error, QueryDsl}; use diesel_async::RunQueryDsl; impl PersonAggregates { - pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { + pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { let conn = &mut get_conn(pool).await?; person_aggregates::table .find(person_id) - .first::(conn) + .first(conn) .await + .optional() } } @@ -127,6 +129,7 @@ mod tests { let person_aggregates_before_delete = PersonAggregates::read(pool, inserted_person.id) .await + .unwrap() .unwrap(); assert_eq!(1, person_aggregates_before_delete.post_count); @@ -140,6 +143,7 @@ mod tests { .unwrap(); let after_post_like_remove = PersonAggregates::read(pool, inserted_person.id) .await + .unwrap() .unwrap(); assert_eq!(0, after_post_like_remove.post_score); @@ -166,6 +170,7 @@ mod tests { let after_parent_comment_removed = PersonAggregates::read(pool, inserted_person.id) .await + .unwrap() .unwrap(); assert_eq!(0, after_parent_comment_removed.comment_count); // TODO: fix person aggregate comment score calculation @@ -178,6 +183,7 @@ mod tests { .unwrap(); let after_parent_comment_delete = PersonAggregates::read(pool, inserted_person.id) .await + .unwrap() .unwrap(); assert_eq!(0, after_parent_comment_delete.comment_count); // TODO: fix person aggregate comment score calculation @@ -193,6 +199,7 @@ mod tests { CommentLike::like(pool, &comment_like).await.unwrap(); let after_comment_add = PersonAggregates::read(pool, inserted_person.id) .await + .unwrap() .unwrap(); assert_eq!(2, after_comment_add.comment_count); // TODO: fix person aggregate comment score calculation @@ -201,6 +208,7 @@ mod tests { Post::delete(pool, inserted_post.id).await.unwrap(); let after_post_delete = PersonAggregates::read(pool, inserted_person.id) .await + .unwrap() .unwrap(); // TODO: fix person aggregate comment score calculation // assert_eq!(0, after_post_delete.comment_score); @@ -222,8 +230,10 @@ mod tests { assert_eq!(1, community_num_deleted); // Should be none found - let after_delete = PersonAggregates::read(pool, inserted_person.id).await; - assert!(after_delete.is_err()); + let after_delete = PersonAggregates::read(pool, inserted_person.id) + .await + .unwrap(); + assert!(after_delete.is_none()); Instance::delete(pool, inserted_instance.id).await.unwrap(); } diff --git a/crates/db_schema/src/aggregates/person_post_aggregates.rs b/crates/db_schema/src/aggregates/person_post_aggregates.rs index 7657dae9e..f6e108ee9 100644 --- a/crates/db_schema/src/aggregates/person_post_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_post_aggregates.rs @@ -1,5 +1,6 @@ use crate::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, + diesel::OptionalExtension, newtypes::{PersonId, PostId}, schema::person_post_aggregates::dsl::{person_id, person_post_aggregates, post_id}, utils::{get_conn, DbPool}, @@ -25,11 +26,12 @@ impl PersonPostAggregates { pool: &mut DbPool<'_>, person_id_: PersonId, post_id_: PostId, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; person_post_aggregates .find((person_id_, post_id_)) - .first::(conn) + .first(conn) .await + .optional() } } diff --git a/crates/db_schema/src/aggregates/post_aggregates.rs b/crates/db_schema/src/aggregates/post_aggregates.rs index 7f95dc05a..cb8227795 100644 --- a/crates/db_schema/src/aggregates/post_aggregates.rs +++ b/crates/db_schema/src/aggregates/post_aggregates.rs @@ -1,5 +1,6 @@ use crate::{ aggregates::structs::PostAggregates, + diesel::OptionalExtension, newtypes::PostId, schema::{community_aggregates, post, post_aggregates}, utils::{ @@ -12,12 +13,13 @@ use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; impl PostAggregates { - pub async fn read(pool: &mut DbPool<'_>, post_id: PostId) -> Result { + pub async fn read(pool: &mut DbPool<'_>, post_id: PostId) -> Result, Error> { let conn = &mut get_conn(pool).await?; post_aggregates::table .find(post_id) - .first::(conn) + .first(conn) .await + .optional() } pub async fn update_ranks(pool: &mut DbPool<'_>, post_id: PostId) -> Result { @@ -141,7 +143,10 @@ mod tests { PostLike::like(pool, &post_like).await.unwrap(); - let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id) + .await + .unwrap() + .unwrap(); assert_eq!(2, post_aggs_before_delete.comments); assert_eq!(1, post_aggs_before_delete.score); @@ -157,7 +162,10 @@ mod tests { PostLike::like(pool, &post_dislike).await.unwrap(); - let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id) + .await + .unwrap() + .unwrap(); assert_eq!(2, post_aggs_after_dislike.comments); assert_eq!(0, post_aggs_after_dislike.score); @@ -169,7 +177,10 @@ mod tests { Comment::delete(pool, inserted_child_comment.id) .await .unwrap(); - let after_comment_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let after_comment_delete = PostAggregates::read(pool, inserted_post.id) + .await + .unwrap() + .unwrap(); assert_eq!(0, after_comment_delete.comments); assert_eq!(0, after_comment_delete.score); assert_eq!(1, after_comment_delete.upvotes); @@ -179,7 +190,10 @@ mod tests { PostLike::remove(pool, inserted_person.id, inserted_post.id) .await .unwrap(); - let after_like_remove = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let after_like_remove = PostAggregates::read(pool, inserted_post.id) + .await + .unwrap() + .unwrap(); assert_eq!(0, after_like_remove.comments); assert_eq!(-1, after_like_remove.score); assert_eq!(0, after_like_remove.upvotes); @@ -199,8 +213,8 @@ mod tests { assert_eq!(1, community_num_deleted); // Should be none found, since the creator was deleted - let after_delete = PostAggregates::read(pool, inserted_post.id).await; - assert!(after_delete.is_err()); + let after_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + assert!(after_delete.is_none()); Instance::delete(pool, inserted_instance.id).await.unwrap(); } @@ -248,7 +262,10 @@ mod tests { let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); - let post_aggregates_before = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggregates_before = PostAggregates::read(pool, inserted_post.id) + .await + .unwrap() + .unwrap(); assert_eq!(1, post_aggregates_before.comments); Comment::update( @@ -262,7 +279,10 @@ mod tests { .await .unwrap(); - let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id) + .await + .unwrap() + .unwrap(); assert_eq!(0, post_aggregates_after_remove.comments); Comment::update( @@ -287,7 +307,10 @@ mod tests { .await .unwrap(); - let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id) + .await + .unwrap() + .unwrap(); assert_eq!(0, post_aggregates_after_delete.comments); Comment::update( @@ -301,8 +324,10 @@ mod tests { .await .unwrap(); - let post_aggregates_after_delete_remove = - PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggregates_after_delete_remove = PostAggregates::read(pool, inserted_post.id) + .await + .unwrap() + .unwrap(); assert_eq!(0, post_aggregates_after_delete_remove.comments); Comment::delete(pool, inserted_comment.id).await.unwrap(); diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index b179ff7c7..268a37aac 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -1,5 +1,6 @@ use crate::{ aggregates::structs::SiteAggregates, + diesel::OptionalExtension, schema::site_aggregates, utils::{get_conn, DbPool}, }; @@ -7,9 +8,9 @@ use diesel::result::Error; use diesel_async::RunQueryDsl; impl SiteAggregates { - pub async fn read(pool: &mut DbPool<'_>) -> Result { + pub async fn read(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - site_aggregates::table.first::(conn).await + site_aggregates::table.first(conn).await.optional() } } @@ -111,7 +112,7 @@ mod tests { .await .unwrap(); - let site_aggregates_before_delete = SiteAggregates::read(pool).await.unwrap(); + let site_aggregates_before_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); // TODO: this is unstable, sometimes it returns 0 users, sometimes 1 //assert_eq!(0, site_aggregates_before_delete.users); @@ -121,7 +122,7 @@ mod tests { // Try a post delete Post::delete(pool, inserted_post.id).await.unwrap(); - let site_aggregates_after_post_delete = SiteAggregates::read(pool).await.unwrap(); + let site_aggregates_after_post_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); assert_eq!(1, site_aggregates_after_post_delete.posts); assert_eq!(0, site_aggregates_after_post_delete.comments); @@ -140,8 +141,8 @@ mod tests { assert!(after_delete_creator.is_ok()); Site::delete(pool, inserted_site.id).await.unwrap(); - let after_delete_site = SiteAggregates::read(pool).await; - assert!(after_delete_site.is_err()); + let after_delete_site = SiteAggregates::read(pool).await.unwrap(); + assert!(after_delete_site.is_none()); Instance::delete(pool, inserted_instance.id).await.unwrap(); } @@ -155,7 +156,7 @@ mod tests { let (inserted_instance, inserted_person, inserted_site, inserted_community) = prepare_site_with_community(pool).await; - let site_aggregates_before = SiteAggregates::read(pool).await.unwrap(); + let site_aggregates_before = SiteAggregates::read(pool).await.unwrap().unwrap(); assert_eq!(1, site_aggregates_before.communities); Community::update( @@ -169,7 +170,7 @@ mod tests { .await .unwrap(); - let site_aggregates_after_delete = SiteAggregates::read(pool).await.unwrap(); + let site_aggregates_after_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); assert_eq!(0, site_aggregates_after_delete.communities); Community::update( @@ -194,7 +195,7 @@ mod tests { .await .unwrap(); - let site_aggregates_after_remove = SiteAggregates::read(pool).await.unwrap(); + let site_aggregates_after_remove = SiteAggregates::read(pool).await.unwrap().unwrap(); assert_eq!(0, site_aggregates_after_remove.communities); Community::update( @@ -208,7 +209,7 @@ mod tests { .await .unwrap(); - let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await.unwrap(); + let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); assert_eq!(0, site_aggregates_after_remove_delete.communities); Community::delete(pool, inserted_community.id) diff --git a/crates/db_schema/src/impls/activity.rs b/crates/db_schema/src/impls/activity.rs index e1802c7c5..9391d55bc 100644 --- a/crates/db_schema/src/impls/activity.rs +++ b/crates/db_schema/src/impls/activity.rs @@ -22,18 +22,22 @@ impl SentActivity { .await } - pub async fn read_from_apub_id(pool: &mut DbPool<'_>, object_id: &DbUrl) -> Result { + pub async fn read_from_apub_id( + pool: &mut DbPool<'_>, + object_id: &DbUrl, + ) -> Result, Error> { use crate::schema::sent_activity::dsl::{ap_id, sent_activity}; let conn = &mut get_conn(pool).await?; sent_activity .filter(ap_id.eq(object_id)) - .first::(conn) + .first(conn) .await + .optional() } - pub async fn read(pool: &mut DbPool<'_>, object_id: ActivityId) -> Result { + pub async fn read(pool: &mut DbPool<'_>, object_id: ActivityId) -> Result, Error> { use crate::schema::sent_activity::dsl::sent_activity; let conn = &mut get_conn(pool).await?; - sent_activity.find(object_id).first::(conn).await + sent_activity.find(object_id).first(conn).await.optional() } } @@ -81,12 +85,9 @@ mod tests { .unwrap() .into(); - // inserting activity for first time - let res = ReceivedActivity::create(pool, &ap_id).await; - assert!(res.is_ok()); - - let res = ReceivedActivity::create(pool, &ap_id).await; - assert!(res.is_err()); + // inserting activity should only work once + ReceivedActivity::create(pool, &ap_id).await.unwrap(); + ReceivedActivity::create(pool, &ap_id).await.unwrap_err(); } #[tokio::test] @@ -118,7 +119,10 @@ mod tests { SentActivity::create(pool, form).await.unwrap(); - let res = SentActivity::read_from_apub_id(pool, &ap_id).await.unwrap(); + let res = SentActivity::read_from_apub_id(pool, &ap_id) + .await + .unwrap() + .unwrap(); assert_eq!(res.ap_id, ap_id); assert_eq!(res.data, data); assert_eq!(res.sensitive, sensitive); diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index fe21cf003..682be2ed0 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -43,20 +43,13 @@ impl LocalUserLanguage { }; let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - let langs = local_user_language - .filter(local_user_id.eq(for_local_user_id)) - .order(language_id) - .select(language_id) - .get_results(conn) - .await?; - convert_read_languages(conn, langs).await - }) as _ - }) - .await + let langs = local_user_language + .filter(local_user_id.eq(for_local_user_id)) + .order(language_id) + .select(language_id) + .get_results(conn) + .await?; + convert_read_languages(conn, langs).await } /// Update the user's languages. @@ -90,24 +83,33 @@ impl LocalUserLanguage { .build_transaction() .run(|conn| { Box::pin(async move { - use crate::schema::local_user_language::dsl::{local_user_id, local_user_language}; - // Clear the current user languages - delete(local_user_language.filter(local_user_id.eq(for_local_user_id))) - .execute(conn) - .await?; + use crate::schema::local_user_language::dsl::{ + language_id, + local_user_id, + local_user_language, + }; + // Delete old languages, not including new languages + let delete_old = delete(local_user_language) + .filter(local_user_id.eq(for_local_user_id)) + .filter(language_id.ne_all(&lang_ids)) + .execute(conn); let forms = lang_ids - .into_iter() - .map(|l| LocalUserLanguageForm { + .iter() + .map(|&l| LocalUserLanguageForm { local_user_id: for_local_user_id, language_id: l, }) .collect::>(); - insert_into(local_user_language) + // Insert new languages + let insert_new = insert_into(local_user_language) .values(forms) - .execute(conn) - .await?; + .on_conflict((language_id, local_user_id)) + .do_nothing() + .execute(conn); + + tokio::try_join!(delete_old, insert_new)?; Ok(()) }) as _ }) @@ -159,25 +161,30 @@ impl SiteLanguage { .build_transaction() .run(|conn| { Box::pin(async move { - use crate::schema::site_language::dsl::{site_id, site_language}; + use crate::schema::site_language::dsl::{language_id, site_id, site_language}; - // Clear the current languages - delete(site_language.filter(site_id.eq(for_site_id))) - .execute(conn) - .await?; + // Delete old languages, not including new languages + let delete_old = delete(site_language) + .filter(site_id.eq(for_site_id)) + .filter(language_id.ne_all(&lang_ids)) + .execute(conn); let forms = lang_ids - .into_iter() - .map(|l| SiteLanguageForm { + .iter() + .map(|&l| SiteLanguageForm { site_id: for_site_id, language_id: l, }) .collect::>(); - insert_into(site_language) + // Insert new languages + let insert_new = insert_into(site_language) .values(forms) - .get_result::(conn) - .await?; + .on_conflict((site_id, language_id)) + .do_nothing() + .execute(conn); + + tokio::try_join!(delete_old, insert_new)?; CommunityLanguage::limit_languages(conn, instance_id).await?; @@ -278,8 +285,8 @@ impl CommunityLanguage { } let form = lang_ids - .into_iter() - .map(|language_id| CommunityLanguageForm { + .iter() + .map(|&language_id| CommunityLanguageForm { community_id: for_community_id, language_id, }) @@ -289,25 +296,25 @@ impl CommunityLanguage { .build_transaction() .run(|conn| { Box::pin(async move { - use crate::schema::community_language::dsl::{community_id, community_language}; - use diesel::result::DatabaseErrorKind::UniqueViolation; - // Clear the current languages - delete(community_language.filter(community_id.eq(for_community_id))) - .execute(conn) - .await?; + use crate::schema::community_language::dsl::{ + community_id, + community_language, + language_id, + }; + // Delete old languages, not including new languages + let delete_old = delete(community_language) + .filter(community_id.eq(for_community_id)) + .filter(language_id.ne_all(&lang_ids)) + .execute(conn); - let insert_res = insert_into(community_language) + // Insert new languages + let insert_new = insert_into(community_language) .values(form) - .get_result::(conn) - .await; + .on_conflict((community_id, language_id)) + .do_nothing() + .execute(conn); - if let Err(Error::DatabaseError(UniqueViolation, _info)) = insert_res { - // race condition: this function was probably called simultaneously from another caller. ignore error - // tracing::warn!("unique error: {_info:#?}"); - // _info.constraint_name() should be = "community_language_community_id_language_id_key" - return Ok(()); - } - insert_res?; + tokio::try_join!(delete_old, insert_new)?; Ok(()) }) as _ diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index c0dad0ad3..eff7da26f 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -1,5 +1,5 @@ use crate::{ - diesel::DecoratableTarget, + diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommentId, DbUrl, PersonId}, schema::comment, source::comment::{ @@ -15,12 +15,7 @@ use crate::{ utils::{functions::coalesce, get_conn, naive_now, DbPool, DELETED_REPLACEMENT_TEXT}, }; use chrono::{DateTime, Utc}; -use diesel::{ - dsl::{insert_into, sql_query}, - result::Error, - ExpressionMethods, - QueryDsl, -}; +use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use diesel_ltree::Ltree; use url::Url; @@ -72,81 +67,23 @@ impl Comment { parent_path: Option<&Ltree>, ) -> Result { let conn = &mut get_conn(pool).await?; + let comment_form = (comment_form, parent_path.map(|p| comment::path.eq(p))); - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - // Insert, to get the id - let inserted_comment = if let Some(timestamp) = timestamp { - insert_into(comment::table) - .values(comment_form) - .on_conflict(comment::ap_id) - .filter_target(coalesce(comment::updated, comment::published).lt(timestamp)) - .do_update() - .set(comment_form) - .get_result::(conn) - .await? - } else { - insert_into(comment::table) - .values(comment_form) - .get_result::(conn) - .await? - }; - - let comment_id = inserted_comment.id; - - // You need to update the ltree column - let ltree = Ltree(if let Some(parent_path) = parent_path { - // The previous parent will already have 0 in it - // Append this comment id - format!("{}.{}", parent_path.0, comment_id) - } else { - // '0' is always the first path, append to that - format!("{}.{}", 0, comment_id) - }); - - let updated_comment = diesel::update(comment::table.find(comment_id)) - .set(comment::path.eq(ltree)) - .get_result::(conn) - .await?; - - // Update the child count for the parent comment_aggregates - // You could do this with a trigger, but since you have to do this manually anyway, - // you can just have it here - if let Some(parent_path) = parent_path { - // You have to update counts for all parents, not just the immediate one - // TODO if the performance of this is terrible, it might be better to do this as part of a - // scheduled query... although the counts would often be wrong. - // - // The child_count query for reference: - // select c.id, c.path, count(c2.id) as child_count from comment c - // left join comment c2 on c2.path <@ c.path and c2.path != c.path - // group by c.id - - let parent_id = parent_path.0.split('.').nth(1); - - if let Some(parent_id) = parent_id { - let top_parent = format!("0.{}", parent_id); - let update_child_count_stmt = format!( - " -update comment_aggregates ca set child_count = c.child_count -from ( - select c.id, c.path, count(c2.id) as child_count from comment c - join comment c2 on c2.path <@ c.path and c2.path != c.path - and c.path <@ '{top_parent}' - group by c.id -) as c -where ca.comment_id = c.id" - ); - - sql_query(update_child_count_stmt).execute(conn).await?; - } - } - Ok(updated_comment) - }) as _ - }) - .await + if let Some(timestamp) = timestamp { + insert_into(comment::table) + .values(comment_form) + .on_conflict(comment::ap_id) + .filter_target(coalesce(comment::updated, comment::published).lt(timestamp)) + .do_update() + .set(comment_form) + .get_result::(conn) + .await + } else { + insert_into(comment::table) + .values(comment_form) + .get_result::(conn) + .await + } } pub async fn read_from_apub_id( @@ -155,14 +92,11 @@ where ca.comment_id = c.id" ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let object_id: DbUrl = object_id.into(); - Ok( - comment::table - .filter(comment::ap_id.eq(object_id)) - .first::(conn) - .await - .ok() - .map(Into::into), - ) + comment::table + .filter(comment::ap_id.eq(object_id)) + .first(conn) + .await + .optional() } pub fn parent_comment_id(&self) -> Option { @@ -400,7 +334,10 @@ mod tests { .await .unwrap(); - let read_comment = Comment::read(pool, inserted_comment.id).await.unwrap(); + let read_comment = Comment::read(pool, inserted_comment.id) + .await + .unwrap() + .unwrap(); let like_removed = CommentLike::remove(pool, inserted_person.id, inserted_comment.id) .await .unwrap(); diff --git a/crates/db_schema/src/impls/comment_reply.rs b/crates/db_schema/src/impls/comment_reply.rs index e6d0915e9..5a33a51d7 100644 --- a/crates/db_schema/src/impls/comment_reply.rs +++ b/crates/db_schema/src/impls/comment_reply.rs @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, newtypes::{CommentId, CommentReplyId, PersonId}, schema::comment_reply, source::comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm}, @@ -21,7 +22,7 @@ impl Crud for CommentReply { let conn = &mut get_conn(pool).await?; // since the return here isnt utilized, we dont need to do an update - // but get_result doesnt return the existing row here + // but get_result doesn't return the existing row here insert_into(comment_reply::table) .values(comment_reply_form) .on_conflict((comment_reply::recipient_id, comment_reply::comment_id)) @@ -63,135 +64,26 @@ impl CommentReply { pub async fn read_by_comment( pool: &mut DbPool<'_>, for_comment_id: CommentId, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; comment_reply::table .filter(comment_reply::comment_id.eq(for_comment_id)) - .first::(conn) + .first(conn) .await + .optional() } pub async fn read_by_comment_and_person( pool: &mut DbPool<'_>, for_comment_id: CommentId, for_recipient_id: PersonId, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; comment_reply::table .filter(comment_reply::comment_id.eq(for_comment_id)) .filter(comment_reply::recipient_id.eq(for_recipient_id)) - .first::(conn) + .first(conn) .await - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] -mod tests { - - use crate::{ - source::{ - comment::{Comment, CommentInsertForm}, - comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm}, - community::{Community, CommunityInsertForm}, - instance::Instance, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - }, - traits::Crud, - utils::build_db_pool_for_tests, - }; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() { - 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 new_person = PersonInsertForm::builder() - .name("terrylake".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let inserted_person = Person::create(pool, &new_person).await.unwrap(); - - let recipient_form = PersonInsertForm::builder() - .name("terrylakes recipient".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let inserted_recipient = Person::create(pool, &recipient_form).await.unwrap(); - - let new_community = CommunityInsertForm::builder() - .name("test community lake".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let inserted_community = Community::create(pool, &new_community).await.unwrap(); - - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); - - let comment_reply_form = CommentReplyInsertForm { - recipient_id: inserted_recipient.id, - comment_id: inserted_comment.id, - read: None, - }; - - let inserted_reply = CommentReply::create(pool, &comment_reply_form) - .await - .unwrap(); - - let expected_reply = CommentReply { - id: inserted_reply.id, - recipient_id: inserted_reply.recipient_id, - comment_id: inserted_reply.comment_id, - read: false, - published: inserted_reply.published, - }; - - let read_reply = CommentReply::read(pool, inserted_reply.id).await.unwrap(); - - let comment_reply_update_form = CommentReplyUpdateForm { read: Some(false) }; - let updated_reply = CommentReply::update(pool, inserted_reply.id, &comment_reply_update_form) - .await - .unwrap(); - - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Post::delete(pool, inserted_post.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Person::delete(pool, inserted_recipient.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); - - assert_eq!(expected_reply, read_reply); - assert_eq!(expected_reply, inserted_reply); - assert_eq!(expected_reply, updated_reply); + .optional() } } diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 4dc45c09b..02b65e6fc 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -1,5 +1,5 @@ use crate::{ - diesel::DecoratableTarget, + diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId}, schema::{community, community_follower, instance}, source::{ @@ -141,29 +141,35 @@ impl Community { Ok(community_) } - /// Get the community which has a given moderators or featured url, also return the collection type + /// Get the community which has a given moderators or featured url, also return the collection + /// type pub async fn get_by_collection_url( pool: &mut DbPool<'_>, url: &DbUrl, - ) -> Result<(Community, CollectionType), Error> { + ) -> 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)) - .first::(conn) - .await; - if let Ok(c) = res { - return Ok((c, Moderators)); + .first(conn) + .await + .optional()?; + + if let Some(c) = res { + Ok(Some((c, Moderators))) + } else { + let res = community::table + .filter(featured_url.eq(url)) + .first(conn) + .await + .optional()?; + if let Some(c) = res { + Ok(Some((c, Featured))) + } else { + Ok(None) + } } - let res = community::table - .filter(featured_url.eq(url)) - .first::(conn) - .await; - if let Ok(c) = res { - return Ok((c, Featured)); - } - Err(diesel::NotFound) } pub async fn set_featured_posts( @@ -348,21 +354,18 @@ impl ApubActor for Community { object_id: &DbUrl, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - Ok( - community::table - .filter(community::actor_id.eq(object_id)) - .first::(conn) - .await - .ok() - .map(Into::into), - ) + community::table + .filter(community::actor_id.eq(object_id)) + .first(conn) + .await + .optional() } async fn read_from_name( pool: &mut DbPool<'_>, community_name: &str, include_deleted: bool, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let mut q = community::table .into_boxed() @@ -373,22 +376,23 @@ impl ApubActor for Community { .filter(community::deleted.eq(false)) .filter(community::removed.eq(false)); } - q.first::(conn).await + q.first(conn).await.optional() } async fn read_from_name_and_domain( pool: &mut DbPool<'_>, community_name: &str, for_domain: &str, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; community::table .inner_join(instance::table) .filter(lower(community::name).eq(community_name.to_lowercase())) .filter(lower(instance::domain).eq(for_domain.to_lowercase())) .select(community::all_columns) - .first::(conn) + .first(conn) .await + .optional() } } @@ -524,7 +528,10 @@ mod tests { expires: None, }; - let read_community = Community::read(pool, inserted_community.id).await.unwrap(); + let read_community = Community::read(pool, inserted_community.id) + .await + .unwrap() + .unwrap(); let update_community_form = CommunityUpdateForm { title: Some("nada".to_owned()), diff --git a/crates/db_schema/src/impls/email_verification.rs b/crates/db_schema/src/impls/email_verification.rs index c5a8792fb..b4951cf73 100644 --- a/crates/db_schema/src/impls/email_verification.rs +++ b/crates/db_schema/src/impls/email_verification.rs @@ -16,6 +16,7 @@ use diesel::{ sql_types::Timestamptz, ExpressionMethods, IntoSql, + OptionalExtension, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -25,17 +26,18 @@ impl EmailVerification { let conn = &mut get_conn(pool).await?; insert_into(email_verification) .values(form) - .get_result::(conn) + .get_result(conn) .await } - pub async fn read_for_token(pool: &mut DbPool<'_>, token: &str) -> Result { + pub async fn read_for_token(pool: &mut DbPool<'_>, token: &str) -> Result, Error> { let conn = &mut get_conn(pool).await?; email_verification .filter(verification_token.eq(token)) .filter(published.gt(now.into_sql::() - 7.days())) - .first::(conn) + .first(conn) .await + .optional() } pub async fn delete_old_tokens_for_local_user( pool: &mut DbPool<'_>, diff --git a/crates/db_schema/src/impls/images.rs b/crates/db_schema/src/impls/images.rs index a9aeb1f16..9589aeee3 100644 --- a/crates/db_schema/src/impls/images.rs +++ b/crates/db_schema/src/impls/images.rs @@ -1,8 +1,8 @@ use crate::{ - newtypes::{DbUrl, LocalUserId}, + newtypes::DbUrl, schema::{local_image, remote_image}, source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm}, - utils::{get_conn, limit_and_offset, DbPool}, + utils::{get_conn, DbPool}, }; use diesel::{ dsl::exists, @@ -25,55 +25,6 @@ impl LocalImage { .await } - pub async fn get_all_paged_by_local_user_id( - pool: &mut DbPool<'_>, - user_id: LocalUserId, - page: Option, - limit: Option, - ) -> Result, Error> { - Self::get_all_helper(pool, Some(user_id), page, limit, false).await - } - - pub async fn get_all_by_local_user_id( - pool: &mut DbPool<'_>, - user_id: LocalUserId, - ) -> Result, Error> { - Self::get_all_helper(pool, Some(user_id), None, None, true).await - } - - pub async fn get_all( - pool: &mut DbPool<'_>, - page: Option, - limit: Option, - ) -> Result, Error> { - Self::get_all_helper(pool, None, page, limit, false).await - } - - async fn get_all_helper( - pool: &mut DbPool<'_>, - user_id: Option, - page: Option, - limit: Option, - ignore_page_limits: bool, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let mut query = local_image::table - .select(local_image::all_columns) - .order_by(local_image::published.desc()) - .into_boxed(); - - if let Some(user_id) = user_id { - query = query.filter(local_image::local_user_id.eq(user_id)) - } - - if !ignore_page_limits { - let (limit, offset) = limit_and_offset(page, limit)?; - query = query.limit(limit).offset(offset); - } - - query.load::(conn).await - } - pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result { let conn = &mut get_conn(pool).await?; diesel::delete(local_image::table.filter(local_image::pictrs_alias.eq(alias))) diff --git a/crates/db_schema/src/impls/instance.rs b/crates/db_schema/src/impls/instance.rs index c329d8cc0..94bf909a3 100644 --- a/crates/db_schema/src/impls/instance.rs +++ b/crates/db_schema/src/impls/instance.rs @@ -26,13 +26,14 @@ use diesel::{ result::Error, ExpressionMethods, NullableExpressionMethods, + OptionalExtension, QueryDsl, SelectableHelper, }; use diesel_async::RunQueryDsl; impl Instance { - /// Attempt to read Instance column for the given domain. If it doesnt exist, insert a new one. + /// Attempt to read Instance column for the given domain. If it doesn't exist, insert a new one. /// There is no need for update as the domain of an existing instance cant change. pub async fn read_or_create(pool: &mut DbPool<'_>, domain_: String) -> Result { use crate::schema::instance::domain; @@ -41,11 +42,14 @@ impl Instance { // First try to read the instance row and return directly if found let instance = instance::table .filter(lower(domain).eq(&domain_.to_lowercase())) - .first::(conn) - .await; + .first(conn) + .await + .optional()?; + + // TODO could convert this to unwrap_or_else once async closures are stable match instance { - Ok(i) => Ok(i), - Err(diesel::NotFound) => { + Some(i) => Ok(i), + None => { // Instance not in database yet, insert it let form = InstanceForm::builder() .domain(domain_) @@ -61,7 +65,6 @@ impl Instance { .get_result::(conn) .await } - e => e, } } pub async fn update( @@ -91,11 +94,15 @@ impl Instance { .await } - #[cfg(test)] + /// Only for use in tests pub async fn delete_all(pool: &mut DbPool<'_>) -> Result { let conn = &mut get_conn(pool).await?; + diesel::delete(federation_queue_state::table) + .execute(conn) + .await?; diesel::delete(instance::table).execute(conn).await } + pub async fn allowlist(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; instance::table @@ -114,15 +121,15 @@ impl Instance { .await } - /// returns a list of all instances, each with a flag of whether the instance is allowed or not and dead or not - /// ordered by id + /// returns a list of all instances, each with a flag of whether the instance is allowed or not + /// and dead or not ordered by id pub async fn read_federated_with_blocked_and_dead( pool: &mut DbPool<'_>, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let is_dead_expr = coalesce(instance::updated, instance::published).lt(now() - 3.days()); - // this needs to be done in two steps because the meaning of the "blocked" column depends on the existence - // of any value at all in the allowlist. (so a normal join wouldn't work) + // this needs to be done in two steps because the meaning of the "blocked" column depends on the + // existence of any value at all in the allowlist. (so a normal join wouldn't work) let use_allowlist = federation_allowlist::table .select(count_star().gt(0)) .get_result::(conn) diff --git a/crates/db_schema/src/impls/language.rs b/crates/db_schema/src/impls/language.rs index 905221402..6a7b4e9ac 100644 --- a/crates/db_schema/src/impls/language.rs +++ b/crates/db_schema/src/impls/language.rs @@ -1,7 +1,7 @@ use crate::{ diesel::ExpressionMethods, newtypes::LanguageId, - schema::language::dsl::{code, language}, + schema::language, source::language::Language, utils::{get_conn, DbPool}, }; @@ -9,14 +9,14 @@ use diesel::{result::Error, QueryDsl}; use diesel_async::RunQueryDsl; impl Language { - pub async fn read_all(pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn read_all(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - language.load::(conn).await + language::table.load(conn).await } - pub async fn read_from_id(pool: &mut DbPool<'_>, id_: LanguageId) -> Result { + pub async fn read_from_id(pool: &mut DbPool<'_>, id_: LanguageId) -> Result { let conn = &mut get_conn(pool).await?; - language.find(id_).first::(conn).await + language::table.find(id_).first(conn).await } /// Attempts to find the given language code and return its ID. If not found, returns none. @@ -27,8 +27,8 @@ impl Language { if let Some(code_) = code_ { let conn = &mut get_conn(pool).await?; Ok( - language - .filter(code.eq(code_)) + language::table + .filter(language::code.eq(code_)) .first::(conn) .await .map(|l| l.id) diff --git a/crates/db_schema/src/impls/local_site.rs b/crates/db_schema/src/impls/local_site.rs index 39ed624ef..602dfe1f4 100644 --- a/crates/db_schema/src/impls/local_site.rs +++ b/crates/db_schema/src/impls/local_site.rs @@ -28,7 +28,7 @@ impl LocalSite { CACHE .try_get_with((), async { let conn = &mut get_conn(pool).await?; - local_site::table.first::(conn).await + local_site::table.first(conn).await }) .await?, ) diff --git a/crates/db_schema/src/impls/local_site_rate_limit.rs b/crates/db_schema/src/impls/local_site_rate_limit.rs index 0c9e96e0b..6ab9ca8b8 100644 --- a/crates/db_schema/src/impls/local_site_rate_limit.rs +++ b/crates/db_schema/src/impls/local_site_rate_limit.rs @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, schema::local_site_rate_limit, source::local_site_rate_limit::{ LocalSiteRateLimit, @@ -11,9 +12,9 @@ use diesel::{dsl::insert_into, result::Error}; use diesel_async::RunQueryDsl; impl LocalSiteRateLimit { - pub async fn read(pool: &mut DbPool<'_>) -> Result { + pub async fn read(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - local_site_rate_limit::table.first::(conn).await + local_site_rate_limit::table.first(conn).await.optional() } pub async fn create( diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index d253afd8d..62fc418d0 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -55,12 +55,17 @@ impl LocalUser { pool: &mut DbPool<'_>, local_user_id: LocalUserId, form: &LocalUserUpdateForm, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(local_user::table.find(local_user_id)) + let res = diesel::update(local_user::table.find(local_user_id)) .set(form) - .get_result::(conn) - .await + .execute(conn) + .await; + // Diesel will throw an error if the query is all Nones (not updating anything), ignore this. + match res { + Err(Error::QueryBuilderError(_)) => Ok(0), + other => other, + } } pub async fn delete(pool: &mut DbPool<'_>, id: LocalUserId) -> Result { 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 4458fc1b6..d77502335 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 @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, newtypes::LocalUserId, schema::local_user_vote_display_mode, source::local_user_vote_display_mode::{ @@ -12,11 +13,12 @@ use diesel::{dsl::insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; impl LocalUserVoteDisplayMode { - pub async fn read(pool: &mut DbPool<'_>) -> Result { + pub async fn read(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; local_user_vote_display_mode::table - .first::(conn) + .first(conn) .await + .optional() } pub async fn create( diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/moderator.rs index 99f117878..125bbcd51 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/moderator.rs @@ -568,6 +568,7 @@ mod tests { .unwrap(); let read_mod_remove_post = ModRemovePost::read(pool, inserted_mod_remove_post.id) .await + .unwrap() .unwrap(); let expected_mod_remove_post = ModRemovePost { id: inserted_mod_remove_post.id, @@ -590,6 +591,7 @@ mod tests { .unwrap(); let read_mod_lock_post = ModLockPost::read(pool, inserted_mod_lock_post.id) .await + .unwrap() .unwrap(); let expected_mod_lock_post = ModLockPost { id: inserted_mod_lock_post.id, @@ -612,6 +614,7 @@ mod tests { .unwrap(); let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id) .await + .unwrap() .unwrap(); let expected_mod_feature_post = ModFeaturePost { id: inserted_mod_feature_post.id, @@ -635,6 +638,7 @@ mod tests { .unwrap(); let read_mod_remove_comment = ModRemoveComment::read(pool, inserted_mod_remove_comment.id) .await + .unwrap() .unwrap(); let expected_mod_remove_comment = ModRemoveComment { id: inserted_mod_remove_comment.id, @@ -660,6 +664,7 @@ mod tests { let read_mod_remove_community = ModRemoveCommunity::read(pool, inserted_mod_remove_community.id) .await + .unwrap() .unwrap(); let expected_mod_remove_community = ModRemoveCommunity { id: inserted_mod_remove_community.id, @@ -687,6 +692,7 @@ mod tests { let read_mod_ban_from_community = ModBanFromCommunity::read(pool, inserted_mod_ban_from_community.id) .await + .unwrap() .unwrap(); let expected_mod_ban_from_community = ModBanFromCommunity { id: inserted_mod_ban_from_community.id, @@ -709,7 +715,10 @@ mod tests { expires: None, }; let inserted_mod_ban = ModBan::create(pool, &mod_ban_form).await.unwrap(); - let read_mod_ban = ModBan::read(pool, inserted_mod_ban.id).await.unwrap(); + let read_mod_ban = ModBan::read(pool, inserted_mod_ban.id) + .await + .unwrap() + .unwrap(); let expected_mod_ban = ModBan { id: inserted_mod_ban.id, mod_person_id: inserted_mod.id, @@ -733,6 +742,7 @@ mod tests { .unwrap(); let read_mod_add_community = ModAddCommunity::read(pool, inserted_mod_add_community.id) .await + .unwrap() .unwrap(); let expected_mod_add_community = ModAddCommunity { id: inserted_mod_add_community.id, @@ -751,7 +761,10 @@ mod tests { removed: None, }; let inserted_mod_add = ModAdd::create(pool, &mod_add_form).await.unwrap(); - let read_mod_add = ModAdd::read(pool, inserted_mod_add.id).await.unwrap(); + let read_mod_add = ModAdd::read(pool, inserted_mod_add.id) + .await + .unwrap() + .unwrap(); let expected_mod_add = ModAdd { id: inserted_mod_add.id, mod_person_id: inserted_mod.id, diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index 491097135..c92cb0867 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -1,82 +1,45 @@ use crate::{ + diesel::OptionalExtension, newtypes::LocalUserId, - schema::password_reset_request::dsl::{local_user_id, password_reset_request, published, token}, + schema::password_reset_request::dsl::{password_reset_request, published, token}, source::password_reset_request::{PasswordResetRequest, PasswordResetRequestForm}, - traits::Crud, utils::{get_conn, DbPool}, }; use diesel::{ + delete, dsl::{insert_into, now, IntervalDsl}, result::Error, sql_types::Timestamptz, ExpressionMethods, IntoSql, - QueryDsl, }; use diesel_async::RunQueryDsl; -#[async_trait] -impl Crud for PasswordResetRequest { - type InsertForm = PasswordResetRequestForm; - type UpdateForm = PasswordResetRequestForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &PasswordResetRequestForm) -> Result { - let conn = &mut get_conn(pool).await?; - insert_into(password_reset_request) - .values(form) - .get_result::(conn) - .await - } - async fn update( - pool: &mut DbPool<'_>, - password_reset_request_id: i32, - form: &PasswordResetRequestForm, - ) -> Result { - let conn = &mut get_conn(pool).await?; - diesel::update(password_reset_request.find(password_reset_request_id)) - .set(form) - .get_result::(conn) - .await - } -} - impl PasswordResetRequest { - pub async fn create_token( + pub async fn create( pool: &mut DbPool<'_>, from_local_user_id: LocalUserId, token_: String, ) -> Result { let form = PasswordResetRequestForm { local_user_id: from_local_user_id, - token: token_, + token: token_.into(), }; - - Self::create(pool, &form).await - } - pub async fn read_from_token( - pool: &mut DbPool<'_>, - token_: &str, - ) -> Result { let conn = &mut get_conn(pool).await?; - password_reset_request + insert_into(password_reset_request) + .values(form) + .get_result::(conn) + .await + } + + pub async fn read_and_delete(pool: &mut DbPool<'_>, token_: &str) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + delete(password_reset_request) .filter(token.eq(token_)) .filter(published.gt(now.into_sql::() - 1.days())) - .first::(conn) - .await - } - - pub async fn get_recent_password_resets_count( - pool: &mut DbPool<'_>, - user_id: LocalUserId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - password_reset_request - .filter(local_user_id.eq(user_id)) - .filter(published.gt(now.into_sql::() - 1.days())) - .count() .get_result(conn) .await + .optional() } } @@ -95,61 +58,64 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_password_reset() -> 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(); - + // Setup + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::builder() .name("thommy prw".into()) .public_key("pubkey".to_string()) .instance_id(inserted_instance.id) .build(); - - let inserted_person = Person::create(pool, &new_person).await.unwrap(); - + 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 inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; - let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]) - .await - .unwrap(); - + // Create password reset token let token = "nope"; - let inserted_password_reset_request = - PasswordResetRequest::create_token(pool, inserted_local_user.id, token.to_string()) - .await - .unwrap(); + PasswordResetRequest::create(pool, inserted_local_user.id, token.to_string()).await?; - let expected_password_reset_request = PasswordResetRequest { - id: inserted_password_reset_request.id, - local_user_id: inserted_local_user.id, - token: token.to_string(), - published: inserted_password_reset_request.published, - }; - - let read_password_reset_request = PasswordResetRequest::read_from_token(pool, token) - .await + // Read it and verify + let read_password_reset_request = PasswordResetRequest::read_and_delete(pool, token) + .await? .unwrap(); - let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); - - assert_eq!(expected_password_reset_request, read_password_reset_request); assert_eq!( - expected_password_reset_request, - inserted_password_reset_request + inserted_password_reset_request.id, + read_password_reset_request.id ); + assert_eq!( + inserted_password_reset_request.local_user_id, + read_password_reset_request.local_user_id + ); + assert_eq!( + inserted_password_reset_request.token, + read_password_reset_request.token + ); + assert_eq!( + inserted_password_reset_request.published, + read_password_reset_request.published + ); + + // Cannot reuse same token again + let read_password_reset_request = PasswordResetRequest::read_and_delete(pool, token).await?; + assert!(read_password_reset_request.is_none()); + + // Cleanup + let num_deleted = Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(1, num_deleted); + Ok(()) } } diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index c90f67891..0255785d6 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, schema::{comment, community, instance, local_user, person, person_follower, post}, source::person::{ @@ -19,13 +20,16 @@ impl Crud for Person { type InsertForm = PersonInsertForm; type UpdateForm = PersonUpdateForm; type IdType = PersonId; - async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { + + // Override this, so that you don't get back deleted + async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { let conn = &mut get_conn(pool).await?; person::table .filter(person::deleted.eq(false)) .find(person_id) - .first::(conn) + .first(conn) .await + .optional() } async fn create(pool: &mut DbPool<'_>, form: &PersonInsertForm) -> Result { @@ -51,7 +55,8 @@ impl Crud for Person { impl Person { /// Update or insert the person. /// - /// This is necessary for federation, because Activitypub doesnt distinguish between these actions. + /// This is necessary for federation, because Activitypub doesn't distinguish between these + /// actions. pub async fn upsert(pool: &mut DbPool<'_>, form: &PersonInsertForm) -> Result { let conn = &mut get_conn(pool).await?; insert_into(person::table) @@ -126,22 +131,19 @@ impl ApubActor for Person { object_id: &DbUrl, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - Ok( - person::table - .filter(person::deleted.eq(false)) - .filter(person::actor_id.eq(object_id)) - .first::(conn) - .await - .ok() - .map(Into::into), - ) + person::table + .filter(person::deleted.eq(false)) + .filter(person::actor_id.eq(object_id)) + .first(conn) + .await + .optional() } async fn read_from_name( pool: &mut DbPool<'_>, from_name: &str, include_deleted: bool, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let mut q = person::table .into_boxed() @@ -150,14 +152,14 @@ impl ApubActor for Person { if !include_deleted { q = q.filter(person::deleted.eq(false)) } - q.first::(conn).await + q.first(conn).await.optional() } async fn read_from_name_and_domain( pool: &mut DbPool<'_>, person_name: &str, for_domain: &str, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; person::table @@ -165,8 +167,9 @@ impl ApubActor for Person { .filter(lower(person::name).eq(person_name.to_lowercase())) .filter(lower(instance::domain).eq(for_domain.to_lowercase())) .select(person::all_columns) - .first::(conn) + .first(conn) .await + .optional() } } @@ -269,7 +272,10 @@ mod tests { instance_id: inserted_instance.id, }; - let read_person = Person::read(pool, inserted_person.id).await.unwrap(); + let read_person = Person::read(pool, inserted_person.id) + .await + .unwrap() + .unwrap(); let update_person_form = PersonUpdateForm { actor_id: Some(inserted_person.actor_id.clone()), diff --git a/crates/db_schema/src/impls/person_mention.rs b/crates/db_schema/src/impls/person_mention.rs index 5524f87ee..433176683 100644 --- a/crates/db_schema/src/impls/person_mention.rs +++ b/crates/db_schema/src/impls/person_mention.rs @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, newtypes::{CommentId, PersonId, PersonMentionId}, schema::person_mention, source::person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm}, @@ -20,7 +21,7 @@ impl Crud for PersonMention { ) -> Result { let conn = &mut get_conn(pool).await?; // since the return here isnt utilized, we dont need to do an update - // but get_result doesnt return the existing row here + // but get_result doesn't return the existing row here insert_into(person_mention::table) .values(person_mention_form) .on_conflict((person_mention::recipient_id, person_mention::comment_id)) @@ -63,125 +64,13 @@ impl PersonMention { pool: &mut DbPool<'_>, for_comment_id: CommentId, for_recipient_id: PersonId, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; person_mention::table .filter(person_mention::comment_id.eq(for_comment_id)) .filter(person_mention::recipient_id.eq(for_recipient_id)) - .first::(conn) + .first(conn) .await - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] -mod tests { - - use crate::{ - source::{ - comment::{Comment, CommentInsertForm}, - community::{Community, CommunityInsertForm}, - instance::Instance, - person::{Person, PersonInsertForm}, - person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm}, - post::{Post, PostInsertForm}, - }, - traits::Crud, - utils::build_db_pool_for_tests, - }; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() { - 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 new_person = PersonInsertForm::builder() - .name("terrylake".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let inserted_person = Person::create(pool, &new_person).await.unwrap(); - - let recipient_form = PersonInsertForm::builder() - .name("terrylakes recipient".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let inserted_recipient = Person::create(pool, &recipient_form).await.unwrap(); - - let new_community = CommunityInsertForm::builder() - .name("test community lake".to_string()) - .title("nada".to_owned()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); - - let inserted_community = Community::create(pool, &new_community).await.unwrap(); - - let new_post = PostInsertForm::builder() - .name("A test post".into()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .build(); - - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - - let comment_form = CommentInsertForm::builder() - .content("A test comment".into()) - .creator_id(inserted_person.id) - .post_id(inserted_post.id) - .build(); - - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); - - let person_mention_form = PersonMentionInsertForm { - recipient_id: inserted_recipient.id, - comment_id: inserted_comment.id, - read: None, - }; - - let inserted_mention = PersonMention::create(pool, &person_mention_form) - .await - .unwrap(); - - let expected_mention = PersonMention { - id: inserted_mention.id, - recipient_id: inserted_mention.recipient_id, - comment_id: inserted_mention.comment_id, - read: false, - published: inserted_mention.published, - }; - - let read_mention = PersonMention::read(pool, inserted_mention.id) - .await - .unwrap(); - - let person_mention_update_form = PersonMentionUpdateForm { read: Some(false) }; - let updated_mention = - PersonMention::update(pool, inserted_mention.id, &person_mention_update_form) - .await - .unwrap(); - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Post::delete(pool, inserted_post.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Person::delete(pool, inserted_recipient.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); - - assert_eq!(expected_mention, read_mention); - assert_eq!(expected_mention, inserted_mention); - assert_eq!(expected_mention, updated_mention); + .optional() } } diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index 596eb62bf..d5f1cba98 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, newtypes::{CommunityId, DbUrl, PersonId, PostId}, schema::{post, post_hide, post_like, post_read, post_saved}, source::post::{ @@ -83,22 +84,6 @@ impl Post { .await } - pub async fn list_for_community( - pool: &mut DbPool<'_>, - the_community_id: CommunityId, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - post::table - .filter(post::community_id.eq(the_community_id)) - .filter(post::deleted.eq(false)) - .filter(post::removed.eq(false)) - .then_order_by(post::featured_community.desc()) - .then_order_by(post::published.desc()) - .limit(FETCH_LIMIT_MAX) - .load::(conn) - .await - } - pub async fn list_featured_for_community( pool: &mut DbPool<'_>, the_community_id: CommunityId, @@ -182,14 +167,11 @@ impl Post { ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let object_id: DbUrl = object_id.into(); - Ok( - post::table - .filter(post::ap_id.eq(object_id)) - .first::(conn) - .await - .ok() - .map(Into::into), - ) + post::table + .filter(post::ap_id.eq(object_id)) + .first(conn) + .await + .optional() } pub async fn fetch_pictrs_posts_for_creator( @@ -517,7 +499,7 @@ mod tests { .unwrap(); assert_eq!(2, marked_as_read); - let read_post = Post::read(pool, inserted_post.id).await.unwrap(); + let read_post = Post::read(pool, inserted_post.id).await.unwrap().unwrap(); let new_post_update = PostUpdateForm { name: Some("A test post".into()), diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index e3ad03972..75c7ce9bc 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -1,5 +1,5 @@ use crate::{ - diesel::DecoratableTarget, + diesel::{DecoratableTarget, OptionalExtension}, newtypes::{DbUrl, PersonId, PrivateMessageId}, schema::private_message, source::private_message::{PrivateMessage, PrivateMessageInsertForm, PrivateMessageUpdateForm}, @@ -9,7 +9,6 @@ use crate::{ use chrono::{DateTime, Utc}; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -use lemmy_utils::error::LemmyResult; use url::Url; #[async_trait] @@ -74,17 +73,14 @@ impl PrivateMessage { pub async fn read_from_apub_id( pool: &mut DbPool<'_>, object_id: Url, - ) -> LemmyResult> { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let object_id: DbUrl = object_id.into(); - Ok( - private_message::table - .filter(private_message::ap_id.eq(object_id)) - .first::(conn) - .await - .ok() - .map(Into::into), - ) + private_message::table + .filter(private_message::ap_id.eq(object_id)) + .first(conn) + .await + .optional() } } @@ -156,6 +152,7 @@ mod tests { let read_private_message = PrivateMessage::read(pool, inserted_private_message.id) .await + .unwrap() .unwrap(); let private_message_update_form = PrivateMessageUpdateForm { diff --git a/crates/db_schema/src/impls/private_message_report.rs b/crates/db_schema/src/impls/private_message_report.rs index c20783db0..b5d8fd039 100644 --- a/crates/db_schema/src/impls/private_message_report.rs +++ b/crates/db_schema/src/impls/private_message_report.rs @@ -46,7 +46,7 @@ impl Reportable for PrivateMessageReport { .await } - // TODO: this is unused because private message doesnt have remove handler + // TODO: this is unused because private message doesn't have remove handler async fn resolve_all_for_object( _pool: &mut DbPool<'_>, _pm_id_: PrivateMessageId, diff --git a/crates/db_schema/src/impls/registration_application.rs b/crates/db_schema/src/impls/registration_application.rs index c4df7ba69..46b7d4bee 100644 --- a/crates/db_schema/src/impls/registration_application.rs +++ b/crates/db_schema/src/impls/registration_application.rs @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, newtypes::LocalUserId, schema::registration_application::dsl::{local_user_id, registration_application}, source::registration_application::{ @@ -43,11 +44,12 @@ impl RegistrationApplication { pub async fn find_by_local_user_id( pool: &mut DbPool<'_>, local_user_id_: LocalUserId, - ) -> Result { + ) -> Result, Error> { let conn = &mut get_conn(pool).await?; registration_application .filter(local_user_id.eq(local_user_id_)) - .first::(conn) + .first(conn) .await + .optional() } } diff --git a/crates/db_schema/src/impls/secret.rs b/crates/db_schema/src/impls/secret.rs index f21c6c487..1365ea838 100644 --- a/crates/db_schema/src/impls/secret.rs +++ b/crates/db_schema/src/impls/secret.rs @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, schema::secret::dsl::secret, source::secret::Secret, utils::{get_conn, DbPool}, @@ -9,12 +10,12 @@ use diesel_async::RunQueryDsl; impl Secret { /// Initialize the Secrets from the DB. /// Warning: You should only call this once. - pub async fn init(pool: &mut DbPool<'_>) -> Result { + pub async fn init(pool: &mut DbPool<'_>) -> Result, Error> { Self::read_secrets(pool).await } - async fn read_secrets(pool: &mut DbPool<'_>) -> Result { + async fn read_secrets(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - secret.first::(conn).await + secret.first(conn).await.optional() } } diff --git a/crates/db_schema/src/impls/site.rs b/crates/db_schema/src/impls/site.rs index 7e9329afb..a371f9e07 100644 --- a/crates/db_schema/src/impls/site.rs +++ b/crates/db_schema/src/impls/site.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{DbUrl, InstanceId, SiteId}, - schema::site::dsl::{actor_id, id, instance_id, site}, + schema::site, source::{ actor_language::SiteLanguage, site::{Site, SiteInsertForm, SiteUpdateForm}, @@ -19,7 +19,7 @@ impl Crud for Site { type IdType = SiteId; /// Use SiteView::read_local, or Site::read_from_apub_id instead - async fn read(_pool: &mut DbPool<'_>, _site_id: SiteId) -> Result { + async fn read(_pool: &mut DbPool<'_>, _site_id: SiteId) -> Result, Error> { unimplemented!() } @@ -31,9 +31,9 @@ impl Crud for Site { let conn = &mut get_conn(pool).await?; // Can't do separate insert/update commands because InsertForm/UpdateForm aren't convertible - let site_ = insert_into(site) + let site_ = insert_into(site::table) .values(form) - .on_conflict(actor_id) + .on_conflict(site::actor_id) .do_update() .set(form) .get_result::(conn) @@ -53,7 +53,7 @@ impl Crud for Site { new_site: &Self::UpdateForm, ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(site.find(site_id)) + diesel::update(site::table.find(site_id)) .set(new_site) .get_result::(conn) .await @@ -66,9 +66,9 @@ impl Site { _instance_id: InstanceId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - site - .filter(instance_id.eq(_instance_id)) - .get_result(conn) + site::table + .filter(site::instance_id.eq(_instance_id)) + .first(conn) .await .optional() } @@ -78,17 +78,20 @@ impl Site { ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - site - .filter(actor_id.eq(object_id)) - .first::(conn) + site::table + .filter(site::actor_id.eq(object_id)) + .first(conn) .await .optional() - .map(Into::into) } pub async fn read_remote_sites(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - site.order_by(id).offset(1).get_results::(conn).await + site::table + .order_by(site::id) + .offset(1) + .get_results::(conn) + .await } /// Instance actor is at the root path, so we simply need to clear the path and other unnecessary diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 05663ff3e..431243221 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -24,6 +24,7 @@ pub mod aggregates; #[cfg(feature = "full")] pub mod impls; pub mod newtypes; +pub mod sensitive; #[cfg(feature = "full")] #[rustfmt::skip] #[allow(clippy::wildcard_imports)] @@ -43,6 +44,9 @@ pub mod traits; #[cfg(feature = "full")] pub mod utils; +#[cfg(feature = "full")] +mod schema_setup; + use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; #[cfg(feature = "full")] diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 96fc23ac6..10abfaec4 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -127,11 +127,13 @@ pub struct LanguageId(pub i32); /// The comment reply id. pub struct CommentReplyId(i32); -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[derive( + Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default, Ord, PartialOrd, +)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The instance id. -pub struct InstanceId(i32); +pub struct InstanceId(pub i32); #[derive( Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default, PartialOrd, Ord, @@ -176,7 +178,7 @@ impl Display for DbUrl { } } -// the project doesnt compile with From +// the project doesn't compile with From #[allow(clippy::from_over_into)] impl Into for Url { fn into(self) -> DbUrl { diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 408ed0540..50bdac751 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -178,7 +178,7 @@ diesel::table! { icon -> Nullable, banner -> Nullable, #[max_length = 255] - followers_url -> Varchar, + followers_url -> Nullable, #[max_length = 255] inbox_url -> Varchar, #[max_length = 255] diff --git a/crates/db_schema/src/schema_setup.rs b/crates/db_schema/src/schema_setup.rs new file mode 100644 index 000000000..3def94060 --- /dev/null +++ b/crates/db_schema/src/schema_setup.rs @@ -0,0 +1,66 @@ +use anyhow::Context; +use diesel::{connection::SimpleConnection, Connection, PgConnection}; +use diesel_migrations::{EmbeddedMigrations, MigrationHarness}; +use lemmy_utils::error::LemmyError; +use tracing::info; + +const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + +/// This SQL code sets up the `r` schema, which contains things that can be safely dropped and +/// replaced instead of being changed using migrations. It may not create or modify things outside +/// of the `r` schema (indicated by `r.` before the name), unless a comment says otherwise. +/// +/// Currently, this code is only run after the server starts and there's at least 1 pending +/// migration to run. This means every time you change something here, you must also create a +/// migration (a blank up.sql file works fine). This behavior will be removed when we implement a +/// better way to avoid useless schema updates and locks. +/// +/// If you add something that depends on something (such as a table) created in a new migration, +/// then down.sql must use `CASCADE` when dropping it. This doesn't need to be fixed in old +/// migrations because the "replaceable-schema" migration runs `DROP SCHEMA IF EXISTS r CASCADE` in +/// down.sql. +const REPLACEABLE_SCHEMA: &[&str] = &[ + "DROP SCHEMA IF EXISTS r CASCADE;", + "CREATE SCHEMA r;", + include_str!("../replaceable_schema/utils.sql"), + include_str!("../replaceable_schema/triggers.sql"), +]; + +pub fn run(db_url: &str) -> Result<(), LemmyError> { + // Migrations don't support async connection + let mut conn = PgConnection::establish(db_url).with_context(|| "Error connecting to database")?; + + // Run all pending migrations except for the newest one, then run the newest one in the same + // transaction as `REPLACEABLE_SCHEMA`. This code will be becone less hacky when the conditional + // setup of things in `REPLACEABLE_SCHEMA` is done without using the number of pending + // migrations. + info!("Running Database migrations (This may take a long time)..."); + let migrations = conn + .pending_migrations(MIGRATIONS) + .map_err(|e| anyhow::anyhow!("Couldn't determine pending migrations: {e}"))?; + for migration in migrations.iter().rev().skip(1).rev() { + conn + .run_migration(migration) + .map_err(|e| anyhow::anyhow!("Couldn't run DB Migrations: {e}"))?; + } + conn.transaction::<_, LemmyError, _>(|conn| { + if let Some(migration) = migrations.last() { + // Migration is run with a savepoint since there's already a transaction + conn + .run_migration(migration) + .map_err(|e| anyhow::anyhow!("Couldn't run DB Migrations: {e}"))?; + } else if !cfg!(debug_assertions) { + // In production, skip running `REPLACEABLE_SCHEMA` to avoid locking things in the schema. In + // CI, always run it because `diesel migration` commands would otherwise prevent it. + return Ok(()); + } + conn + .batch_execute(&REPLACEABLE_SCHEMA.join("\n")) + .context("Couldn't run SQL files in crates/db_schema/replaceable_schema")?; + + Ok(()) + })?; + info!("Database migrations complete."); + + Ok(()) +} diff --git a/crates/db_schema/src/sensitive.rs b/crates/db_schema/src/sensitive.rs new file mode 100644 index 000000000..340679e2f --- /dev/null +++ b/crates/db_schema/src/sensitive.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, ops::Deref}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +#[serde(transparent)] +pub struct SensitiveString(String); + +impl SensitiveString { + pub fn into_inner(self) -> String { + self.0 + } +} + +impl Debug for SensitiveString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Sensitive").finish() + } +} + +impl AsRef<[u8]> for SensitiveString { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Deref for SensitiveString { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for SensitiveString { + fn from(t: String) -> Self { + SensitiveString(t) + } +} + +#[cfg(feature = "full")] +impl TS for SensitiveString { + fn name() -> String { + "string".to_string() + } + fn name_with_type_args(_args: Vec) -> String { + "string".to_string() + } + fn dependencies() -> Vec { + Vec::new() + } + fn transparent() -> bool { + true + } +} diff --git a/crates/db_schema/src/source/comment.rs b/crates/db_schema/src/source/comment.rs index 3ebea42c1..74ae0b7f6 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -41,7 +41,8 @@ pub struct Comment { #[cfg(feature = "full")] #[cfg_attr(feature = "full", serde(with = "LtreeDef"))] #[cfg_attr(feature = "full", ts(type = "string"))] - /// The path / tree location of a comment, separated by dots, ending with the comment's id. Ex: 0.24.27 + /// The path / tree location of a comment, separated by dots, ending with the comment's id. Ex: + /// 0.24.27 pub path: Ltree, #[cfg(not(feature = "full"))] pub path: String, diff --git a/crates/db_schema/src/source/community.rs b/crates/db_schema/src/source/community.rs index 434fa7037..fe7f120ec 100644 --- a/crates/db_schema/src/source/community.rs +++ b/crates/db_schema/src/source/community.rs @@ -2,6 +2,7 @@ use crate::schema::{community, community_follower, community_moderator, community_person_ban}; use crate::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, + sensitive::SensitiveString, source::placeholder_apub_url, CommunityVisibility, }; @@ -39,7 +40,7 @@ pub struct Community { /// Whether the community is local. pub local: bool, #[serde(skip)] - pub private_key: Option, + pub private_key: Option, #[serde(skip)] pub public_key: String, #[serde(skip)] @@ -49,8 +50,8 @@ pub struct Community { /// A URL for a banner. pub banner: Option, #[cfg_attr(feature = "full", ts(skip))] - #[serde(skip, default = "placeholder_apub_url")] - pub followers_url: DbUrl, + #[serde(skip)] + pub followers_url: Option, #[cfg_attr(feature = "full", ts(skip))] #[serde(skip, default = "placeholder_apub_url")] pub inbox_url: DbUrl, diff --git a/crates/db_schema/src/source/images.rs b/crates/db_schema/src/source/images.rs index da6be2a14..9d48e011b 100644 --- a/crates/db_schema/src/source/images.rs +++ b/crates/db_schema/src/source/images.rs @@ -22,7 +22,7 @@ use typed_builder::TypedBuilder; diesel(belongs_to(crate::source::local_user::LocalUser)) )] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", diesel(primary_key(local_user_id)))] +#[cfg_attr(feature = "full", diesel(primary_key(pictrs_alias)))] pub struct LocalImage { pub local_user_id: Option, pub pictrs_alias: String, @@ -41,9 +41,11 @@ pub struct LocalImageForm { /// Stores all images which are hosted on remote domains. When attempting to proxy an image, it /// is checked against this table to avoid Lemmy being used as a general purpose proxy. -#[derive(PartialEq, Eq, Debug, Clone)] -#[cfg_attr(feature = "full", derive(Queryable, Identifiable))] +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[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)))] pub struct RemoteImage { pub id: i32, pub link: DbUrl, diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index f159232f7..5d592ddf1 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -2,6 +2,7 @@ use crate::schema::local_user; use crate::{ newtypes::{LocalUserId, PersonId}, + sensitive::SensitiveString, ListingType, PostListingMode, SortType, @@ -24,8 +25,8 @@ pub struct LocalUser { /// The person_id for the local user. pub person_id: PersonId, #[serde(skip)] - pub password_encrypted: String, - pub email: Option, + pub password_encrypted: SensitiveString, + pub email: Option, /// Whether to show NSFW content. pub show_nsfw: bool, pub theme: String, @@ -47,7 +48,7 @@ pub struct LocalUser { /// Whether their registration application has been accepted. pub accepted_application: bool, #[serde(skip)] - pub totp_2fa_secret: Option, + pub totp_2fa_secret: Option, /// Open links in a new tab. pub open_links_in_new_tab: bool, pub blur_nsfw: bool, @@ -61,7 +62,8 @@ pub struct LocalUser { pub totp_2fa_enabled: bool, /// Whether to allow keyboard navigation (for browsing and interacting with posts and comments). pub enable_keyboard_navigation: bool, - /// Whether user avatars and inline images in the UI that are gifs should be allowed to play or should be paused + /// Whether user avatars and inline images in the UI that are gifs should be allowed to play or + /// should be paused pub enable_animated_images: bool, /// Whether to auto-collapse bot comments. pub collapse_bot_comments: bool, diff --git a/crates/db_schema/src/source/login_token.rs b/crates/db_schema/src/source/login_token.rs index e5d61f7c3..38aac33ef 100644 --- a/crates/db_schema/src/source/login_token.rs +++ b/crates/db_schema/src/source/login_token.rs @@ -1,6 +1,6 @@ -use crate::newtypes::LocalUserId; #[cfg(feature = "full")] use crate::schema::login_token; +use crate::{newtypes::LocalUserId, sensitive::SensitiveString}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -18,7 +18,7 @@ use ts_rs::TS; pub struct LoginToken { /// Jwt token for this login #[serde(skip)] - pub token: String, + pub token: SensitiveString, pub user_id: LocalUserId, /// Time of login pub published: DateTime, @@ -31,7 +31,7 @@ pub struct LoginToken { #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = login_token))] pub struct LoginTokenCreateForm { - pub token: String, + pub token: SensitiveString, pub user_id: LocalUserId, pub ip: Option, pub user_agent: Option, diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 3a6501717..bbc8aafa2 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -43,7 +43,8 @@ pub mod tagline; /// Default value for columns like [community::Community.inbox_url] which are marked as serde(skip). /// /// This is necessary so they can be successfully deserialized from API responses, even though the -/// value is not sent by Lemmy. Necessary for crates which rely on Rust API such as lemmy-stats-crawler. +/// value is not sent by Lemmy. Necessary for crates which rely on Rust API such as +/// lemmy-stats-crawler. fn placeholder_apub_url() -> DbUrl { DbUrl(Box::new( Url::parse("http://example.com").expect("parse placeholder url"), diff --git a/crates/db_schema/src/source/password_reset_request.rs b/crates/db_schema/src/source/password_reset_request.rs index edc030e9c..dbc930b8e 100644 --- a/crates/db_schema/src/source/password_reset_request.rs +++ b/crates/db_schema/src/source/password_reset_request.rs @@ -1,6 +1,6 @@ -use crate::newtypes::LocalUserId; #[cfg(feature = "full")] use crate::schema::password_reset_request; +use crate::{newtypes::LocalUserId, sensitive::SensitiveString}; use chrono::{DateTime, Utc}; #[derive(PartialEq, Eq, Debug)] @@ -9,7 +9,7 @@ use chrono::{DateTime, Utc}; #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PasswordResetRequest { pub id: i32, - pub token: String, + pub token: SensitiveString, pub published: DateTime, pub local_user_id: LocalUserId, } @@ -18,5 +18,5 @@ pub struct PasswordResetRequest { #[cfg_attr(feature = "full", diesel(table_name = password_reset_request))] pub struct PasswordResetRequestForm { pub local_user_id: LocalUserId, - pub token: String, + pub token: SensitiveString, } diff --git a/crates/db_schema/src/source/person.rs b/crates/db_schema/src/source/person.rs index 25e65ae79..45eacd4fc 100644 --- a/crates/db_schema/src/source/person.rs +++ b/crates/db_schema/src/source/person.rs @@ -2,6 +2,7 @@ use crate::schema::{person, person_follower}; use crate::{ newtypes::{DbUrl, InstanceId, PersonId}, + sensitive::SensitiveString, source::placeholder_apub_url, }; use chrono::{DateTime, Utc}; @@ -36,7 +37,7 @@ pub struct Person { /// Whether the person is local to our site. pub local: bool, #[serde(skip)] - pub private_key: Option, + pub private_key: Option, #[serde(skip)] pub public_key: String, #[serde(skip)] diff --git a/crates/db_schema/src/source/secret.rs b/crates/db_schema/src/source/secret.rs index 164670a24..36c0b691b 100644 --- a/crates/db_schema/src/source/secret.rs +++ b/crates/db_schema/src/source/secret.rs @@ -1,5 +1,6 @@ #[cfg(feature = "full")] use crate::schema::secret; +use crate::sensitive::SensitiveString; #[derive(Clone)] #[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable))] @@ -7,5 +8,5 @@ use crate::schema::secret; #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct Secret { pub id: i32, - pub jwt_secret: String, + pub jwt_secret: SensitiveString, } diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs index 83d23c779..325bff97c 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -1,6 +1,9 @@ -use crate::newtypes::{DbUrl, InstanceId, SiteId}; #[cfg(feature = "full")] use crate::schema::site; +use crate::{ + newtypes::{DbUrl, InstanceId, SiteId}, + sensitive::SensitiveString, +}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -35,7 +38,7 @@ pub struct Site { /// The site inbox pub inbox_url: DbUrl, #[serde(skip)] - pub private_key: Option, + pub private_key: Option, // TODO: mark as `serde(skip)` in next major release as its not needed for api pub public_key: String, pub instance_id: InstanceId, @@ -73,7 +76,8 @@ pub struct SiteUpdateForm { pub name: Option, pub sidebar: Option>, pub updated: Option>>, - // when you want to null out a column, you have to send Some(None)), since sending None means you just don't want to update that column. + // when you want to null out a column, you have to send Some(None)), since sending None means you + // just don't want to update that column. pub icon: Option>, pub banner: Option>, pub description: Option>, diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index b0434a65c..2b0da6c7f 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -1,4 +1,5 @@ use crate::{ + diesel::OptionalExtension, newtypes::{CommunityId, DbUrl, PersonId}, utils::{get_conn, DbPool}, }; @@ -24,8 +25,8 @@ pub type Find = dsl::Find<::Table, ::IdType>; pub type PrimaryKey = <::Table as Table>::PrimaryKey; -// Trying to create default implementations for `create` and `update` results in a lifetime mess and weird compile errors. -// https://github.com/rust-lang/rust/issues/102211 +// Trying to create default implementations for `create` and `update` results in a lifetime mess and +// weird compile errors. https://github.com/rust-lang/rust/issues/102211 #[async_trait] pub trait Crud: HasTable + Sized where @@ -42,13 +43,14 @@ where async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result; - async fn read(pool: &mut DbPool<'_>, id: Self::IdType) -> Result { + async fn read(pool: &mut DbPool<'_>, id: Self::IdType) -> Result, Error> { let query: Find = Self::table().find(id); let conn = &mut *get_conn(pool).await?; - query.first::(conn).await + query.first(conn).await.optional() } - /// when you want to null out a column, you have to send Some(None)), since sending None means you just don't want to update that column. + /// when you want to null out a column, you have to send Some(None)), since sending None means you + /// just don't want to update that column. async fn update( pool: &mut DbPool<'_>, id: Self::IdType, @@ -185,14 +187,14 @@ pub trait ApubActor { pool: &mut DbPool<'_>, actor_name: &str, include_deleted: bool, - ) -> Result + ) -> Result, Error> where Self: Sized; async fn read_from_name_and_domain( pool: &mut DbPool<'_>, actor_name: &str, protocol_domain: &str, - ) -> Result + ) -> Result, Error> where Self: Sized; } diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index f0b294c04..c6170fcd4 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,11 +1,4 @@ -use crate::{ - diesel::Connection, - diesel_migrations::MigrationHarness, - newtypes::DbUrl, - CommentSortType, - SortType, -}; -use anyhow::Context; +use crate::{newtypes::DbUrl, CommentSortType, SortType}; use chrono::{DateTime, TimeDelta, Utc}; use deadpool::Runtime; use diesel::{ @@ -13,10 +6,14 @@ use diesel::{ pg::Pg, query_builder::{Query, QueryFragment}, query_dsl::methods::LimitDsl, - result::{ConnectionError, ConnectionResult, Error as DieselError, Error::QueryBuilderError}, + result::{ + ConnectionError, + ConnectionResult, + Error::{self as DieselError, QueryBuilderError}, + }, sql_types::{self, Timestamptz}, IntoSql, - PgConnection, + OptionalExtension, }; use diesel_async::{ pg::AsyncPgConnection, @@ -27,7 +24,6 @@ use diesel_async::{ }, SimpleAsyncConnection, }; -use diesel_migrations::EmbeddedMigrations; use futures_util::{future::BoxFuture, Future, FutureExt}; use i_love_jesus::CursorKey; use lemmy_utils::{ @@ -37,15 +33,24 @@ use lemmy_utils::{ use once_cell::sync::Lazy; use regex::Regex; use rustls::{ - client::{ServerCertVerified, ServerCertVerifier}, - ServerName, + client::danger::{ + DangerousClientConfigBuilder, + HandshakeSignatureValid, + ServerCertVerified, + ServerCertVerifier, + }, + crypto::{self, verify_tls12_signature, verify_tls13_signature}, + pki_types::{CertificateDer, ServerName, UnixTime}, + ClientConfig, + DigitallySignedStruct, + SignatureScheme, }; use std::{ ops::{Deref, DerefMut}, sync::Arc, - time::{Duration, SystemTime}, + time::Duration, }; -use tracing::{error, info}; +use tracing::error; use url::Url; const FETCH_LIMIT_DEFAULT: i64 = 10; @@ -56,7 +61,8 @@ pub const RANK_DEFAULT: f64 = 0.0001; pub type ActualDbPool = Pool; -/// References a pool or connection. Functions must take `&mut DbPool<'_>` to allow implicit reborrowing. +/// References a pool or connection. Functions must take `&mut DbPool<'_>` to allow implicit +/// reborrowing. /// /// https://github.com/rust-lang/rfcs/issues/1403 pub enum DbPool<'a> { @@ -96,7 +102,8 @@ impl<'a> DerefMut for DbConn<'a> { } } -// Allows functions that take `DbPool<'_>` to be called in a transaction by passing `&mut conn.into()` +// Allows functions that take `DbPool<'_>` to be called in a transaction by passing `&mut +// conn.into()` impl<'a> From<&'a mut AsyncPgConnection> for DbPool<'a> { fn from(value: &'a mut AsyncPgConnection) -> Self { DbPool::Conn(value) @@ -115,11 +122,13 @@ impl<'a> From<&'a ActualDbPool> for DbPool<'a> { } } -/// Runs multiple async functions that take `&mut DbPool<'_>` as input and return `Result`. Only works when the `futures` crate is listed in `Cargo.toml`. +/// Runs multiple async functions that take `&mut DbPool<'_>` as input and return `Result`. Only +/// works when the `futures` crate is listed in `Cargo.toml`. /// /// `$pool` is the value given to each function. /// -/// A `Result` is returned (not in a `Future`, so don't use `.await`). The `Ok` variant contains a tuple with the values returned by the given functions. +/// A `Result` is returned (not in a `Future`, so don't use `.await`). The `Ok` variant contains a +/// tuple with the values returned by the given functions. /// /// The functions run concurrently if `$pool` has the `DbPool::Pool` variant. #[macro_export] @@ -316,10 +325,11 @@ pub fn diesel_option_overwrite_to_url_create(opt: &Option) -> LemmyResul fn establish_connection(config: &str) -> BoxFuture> { let fut = async { - let rustls_config = rustls::ClientConfig::builder() - .with_safe_defaults() - .with_custom_certificate_verifier(Arc::new(NoCertVerifier {})) - .with_no_client_auth(); + 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) @@ -331,8 +341,10 @@ fn establish_connection(config: &str) -> BoxFuture BoxFuture, - _ocsp_response: &[u8], - _now: SystemTime, + _ocsp: &[u8], + _now: UnixTime, ) -> Result { // Will verify all (even invalid) certs without any checks (sslmode=require) Ok(ServerCertVerified::assertion()) } -} -pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &crypto::ring::default_provider().signature_verification_algorithms, + ) + } -fn run_migrations(db_url: &str) -> LemmyResult<()> { - // Needs to be a sync connection - let mut conn = PgConnection::establish(db_url).with_context(|| "Error connecting to database")?; + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &crypto::ring::default_provider().signature_verification_algorithms, + ) + } - info!("Running Database migrations (This may take a long time)..."); - conn - .run_pending_migrations(MIGRATIONS) - .map_err(|e| anyhow::anyhow!("Couldn't run DB Migrations: {e}"))?; - info!("Database migrations complete."); - - Ok(()) + fn supported_verify_schemes(&self) -> Vec { + crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } } pub async fn build_db_pool() -> LemmyResult { @@ -390,9 +421,11 @@ pub async fn build_db_pool() -> LemmyResult { let pool = Pool::builder(manager) .max_size(SETTINGS.database.pool_size) .runtime(Runtime::Tokio1) - // Limit connection age to prevent use of prepared statements that have query plans based on very old statistics + // Limit connection age to prevent use of prepared statements that have query plans based on + // very old statistics .pre_recycle(Hook::sync_fn(|_conn, metrics| { - // Preventing the first recycle can cause an infinite loop when trying to get a new connection from the pool + // Preventing the first recycle can cause an infinite loop when trying to get a new connection + // from the pool let conn_was_used = metrics.recycled.is_some(); if metrics.age() > Duration::from_secs(3 * 24 * 60 * 60) && conn_was_used { Err(HookError::Continue(None)) @@ -402,7 +435,7 @@ pub async fn build_db_pool() -> LemmyResult { })) .build()?; - run_migrations(&db_url)?; + crate::schema_setup::run(&db_url)?; Ok(pool) } @@ -444,14 +477,17 @@ pub mod functions { use diesel::sql_types::{BigInt, Text, Timestamptz}; sql_function! { + #[sql_name = "r.hot_rank"] fn hot_rank(score: BigInt, time: Timestamptz) -> Double; } sql_function! { + #[sql_name = "r.scaled_rank"] fn scaled_rank(score: BigInt, time: Timestamptz, users_active_month: BigInt) -> Double; } sql_function! { + #[sql_name = "r.controversy_rank"] fn controversy_rank(upvotes: BigInt, downvotes: BigInt, score: BigInt) -> Double; } @@ -480,7 +516,8 @@ pub trait ListFn<'a, T, Args>: Fn(DbConn<'a>, Args) -> ResultFuture<'a, Vec> impl<'a, T, Args, F: Fn(DbConn<'a>, Args) -> ResultFuture<'a, Vec>> ListFn<'a, T, Args> for F {} -/// Allows read and list functions to capture a shared closure that has an inferred return type, which is useful for join logic +/// Allows read and list functions to capture a shared closure that has an inferred return type, +/// which is useful for join logic pub struct Queries { pub read_fn: RF, pub list_fn: LF, @@ -510,12 +547,12 @@ impl Queries { self, pool: &'a mut DbPool<'_>, args: Args, - ) -> Result + ) -> Result, DieselError> where RF: ReadFn<'a, T, Args>, { let conn = get_conn(pool).await?; - (self.read_fn)(conn, args).await + (self.read_fn)(conn, args).await.optional() } pub async fn list<'a, T, Args>( diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 0da5b3cff..ebea1a5f4 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -134,7 +134,7 @@ fn queries<'a>() -> Queries< comment_report::table.find(report_id).into_boxed(), my_person_id, ) - .first::(&mut conn) + .first(&mut conn) .await }; @@ -150,7 +150,8 @@ fn queries<'a>() -> Queries< query = query.filter(comment_report::comment_id.eq(comment_id)); } - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest first (FIFO) + // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) if options.unresolved_only { query = query .filter(comment_report::resolved.eq(false)) @@ -191,7 +192,7 @@ impl CommentReportView { pool: &mut DbPool<'_>, report_id: CommentReportId, my_person_id: PersonId, - ) -> Result { + ) -> Result, Error> { queries().read(pool, (report_id, my_person_id)).await } @@ -396,11 +397,13 @@ mod tests { let agg = CommentAggregates::read(pool, inserted_comment.id) .await + .unwrap() .unwrap(); let read_jessica_report_view = CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) .await + .unwrap() .unwrap(); let expected_jessica_report_view = CommentReportView { comment_report: inserted_jessica_report.clone(), @@ -554,6 +557,7 @@ mod tests { let read_jessica_report_view_after_resolve = CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) .await + .unwrap() .unwrap(); let mut expected_jessica_report_view_after_resolve = expected_jessica_report_view; diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 01ff394eb..7588943b9 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -191,7 +191,7 @@ fn queries<'a>() -> Queries< if my_person_id.is_none() { query = query.filter(community::visibility.eq(CommunityVisibility::Public)); } - query.first::(&mut conn).await + query.first(&mut conn).await }; let list = move |mut conn: DbConn<'a>, options: CommentQuery<'a>| async move { @@ -215,9 +215,13 @@ fn queries<'a>() -> Queries< if let Some(parent_path) = options.parent_path.as_ref() { query = query.filter(comment::path.contained_by(parent_path)); }; - + //filtering out removed and deleted comments from search if let Some(search_term) = options.search_term { - query = query.filter(comment::content.ilike(fuzzy_search(&search_term))); + query = query.filter( + comment::content + .ilike(fuzzy_search(&search_term)) + .and(not(comment::removed.or(comment::deleted))), + ); }; if let Some(community_id) = options.community_id { @@ -234,7 +238,7 @@ fn queries<'a>() -> Queries< ); match listing_type { - ListingType::Subscribed => query = query.filter(is_subscribed), // TODO could be this: and(community_follower::person_id.eq(person_id_join)), + ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ ListingType::Local => { query = query .filter(community::local.eq(true)) @@ -260,10 +264,13 @@ fn queries<'a>() -> Queries< .then_order_by(is_saved(person_id_join).desc()); } - if options.liked_only { - query = query.filter(score(person_id_join).eq(1)); - } else if options.disliked_only { - query = query.filter(score(person_id_join).eq(-1)); + if let Some(my_id) = my_person_id { + let not_creator_filter = comment::creator_id.ne(my_id); + if options.liked_only { + query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); + } else if options.disliked_only { + query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); + } } if !options @@ -320,7 +327,8 @@ fn queries<'a>() -> Queries< query = query.filter(nlevel(comment::path).le(depth_limit)); - // only order if filtering by a post id, or parent_path. DOS potential otherwise and max_depth + !post_id isn't used anyways (afaik) + // only order if filtering by a post id, or parent_path. DOS potential otherwise and max_depth + // + !post_id isn't used anyways (afaik) if options.post_id.is_some() || options.parent_path.is_some() { // Always order by the parent path first query = query.then_order_by(subpath(comment::path, 0, -1)); @@ -375,17 +383,21 @@ impl CommentView { pool: &mut DbPool<'_>, comment_id: CommentId, my_person_id: Option, - ) -> Result { + ) -> 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 - let mut res = queries().read(pool, (comment_id, my_person_id)).await?; - if my_person_id.is_some() && res.my_vote.is_none() { - res.my_vote = Some(0); + if let Ok(Some(res)) = queries().read(pool, (comment_id, my_person_id)).await { + let mut new_view = res.clone(); + if my_person_id.is_some() && res.my_vote.is_none() { + new_view.my_vote = Some(0); + } + if res.comment.deleted || res.comment.removed { + new_view.comment.content = String::new(); + } + Ok(Some(new_view)) + } else { + Ok(None) } - if res.comment.deleted || res.comment.removed { - res.comment.content = String::new(); - } - Ok(res) } } @@ -426,7 +438,6 @@ impl<'a> CommentQuery<'a> { } #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { @@ -472,7 +483,7 @@ mod tests { CommunityVisibility, SubscribedType, }; - use lemmy_utils::error::LemmyResult; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use pretty_assertions::assert_eq; use serial_test::serial; @@ -487,32 +498,28 @@ mod tests { inserted_community: Community, } - async fn init_data(pool: &mut DbPool<'_>) -> Data { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let timmy_person_form = PersonInsertForm::builder() .name("timmy".into()) .public_key("pubkey".to_string()) .instance_id(inserted_instance.id) .build(); - let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap(); + let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?; let timmy_local_user_form = LocalUserInsertForm::builder() .person_id(inserted_timmy_person.id) .admin(Some(true)) .password_encrypted(String::new()) .build(); - let inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]) - .await - .unwrap(); + let inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; let sara_person_form = PersonInsertForm::builder() .name("sara".into()) .public_key("pubkey".to_string()) .instance_id(inserted_instance.id) .build(); - let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap(); + let inserted_sara_person = Person::create(pool, &sara_person_form).await?; let new_community = CommunityInsertForm::builder() .name("test community 5".to_string()) @@ -521,7 +528,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 new_post = PostInsertForm::builder() .name("A test post 2".into()) @@ -529,8 +536,8 @@ mod tests { .community_id(inserted_community.id) .build(); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - let english_id = Language::read_id_from_code(pool, Some("en")).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; + let english_id = Language::read_id_from_code(pool, Some("en")).await?; // Create a comment tree with this hierarchy // 0 @@ -547,7 +554,7 @@ mod tests { .language_id(english_id) .build(); - let inserted_comment_0 = Comment::create(pool, &comment_form_0, None).await.unwrap(); + let inserted_comment_0 = Comment::create(pool, &comment_form_0, None).await?; let comment_form_1 = CommentInsertForm::builder() .content("Comment 1, A test blocked comment".into()) @@ -556,11 +563,10 @@ mod tests { .language_id(english_id) .build(); - let inserted_comment_1 = Comment::create(pool, &comment_form_1, Some(&inserted_comment_0.path)) - .await - .unwrap(); + let inserted_comment_1 = + Comment::create(pool, &comment_form_1, Some(&inserted_comment_0.path)).await?; - let finnish_id = Language::read_id_from_code(pool, Some("fi")).await.unwrap(); + let finnish_id = Language::read_id_from_code(pool, Some("fi")).await?; let comment_form_2 = CommentInsertForm::builder() .content("Comment 2".into()) .creator_id(inserted_timmy_person.id) @@ -568,9 +574,8 @@ mod tests { .language_id(finnish_id) .build(); - let inserted_comment_2 = Comment::create(pool, &comment_form_2, Some(&inserted_comment_0.path)) - .await - .unwrap(); + let inserted_comment_2 = + Comment::create(pool, &comment_form_2, Some(&inserted_comment_0.path)).await?; let comment_form_3 = CommentInsertForm::builder() .content("Comment 3".into()) @@ -580,14 +585,11 @@ mod tests { .build(); let _inserted_comment_3 = - Comment::create(pool, &comment_form_3, Some(&inserted_comment_1.path)) - .await - .unwrap(); + Comment::create(pool, &comment_form_3, Some(&inserted_comment_1.path)).await?; let polish_id = Language::read_id_from_code(pool, Some("pl")) - .await - .unwrap() - .unwrap(); + .await? + .ok_or(LemmyErrorType::LanguageNotAllowed)?; let comment_form_4 = CommentInsertForm::builder() .content("Comment 4".into()) .creator_id(inserted_timmy_person.id) @@ -595,9 +597,8 @@ mod tests { .language_id(Some(polish_id)) .build(); - let inserted_comment_4 = Comment::create(pool, &comment_form_4, Some(&inserted_comment_1.path)) - .await - .unwrap(); + let inserted_comment_4 = + Comment::create(pool, &comment_form_4, Some(&inserted_comment_1.path)).await?; let comment_form_5 = CommentInsertForm::builder() .content("Comment 5".into()) @@ -606,18 +607,14 @@ mod tests { .build(); let _inserted_comment_5 = - Comment::create(pool, &comment_form_5, Some(&inserted_comment_4.path)) - .await - .unwrap(); + Comment::create(pool, &comment_form_5, Some(&inserted_comment_4.path)).await?; let timmy_blocks_sara_form = PersonBlockForm { person_id: inserted_timmy_person.id, target_id: inserted_sara_person.id, }; - let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form) - .await - .unwrap(); + let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form).await?; let expected_block = PersonBlock { person_id: inserted_timmy_person.id, @@ -633,7 +630,7 @@ mod tests { score: 1, }; - let _inserted_comment_like = CommentLike::like(pool, &comment_like_form).await.unwrap(); + let _inserted_comment_like = CommentLike::like(pool, &comment_like_form).await?; let timmy_local_user_view = LocalUserView { local_user: inserted_timmy_local_user.clone(), @@ -641,7 +638,7 @@ mod tests { person: inserted_timmy_person.clone(), counts: Default::default(), }; - Data { + Ok(Data { inserted_instance, inserted_comment_0, inserted_comment_1, @@ -650,7 +647,7 @@ mod tests { timmy_local_user_view, inserted_sara_person, inserted_community, - } + }) } #[tokio::test] @@ -658,7 +655,7 @@ mod tests { async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; let expected_comment_view_no_person = expected_comment_view(&data, pool).await?; @@ -674,8 +671,10 @@ mod tests { .await?; assert_eq!( - expected_comment_view_no_person, - read_comment_views_no_person[0] + &expected_comment_view_no_person, + read_comment_views_no_person + .first() + .ok_or(LemmyErrorType::CouldntFindComment)? ); let read_comment_views_with_person = CommentQuery { @@ -700,23 +699,51 @@ mod tests { data.inserted_comment_1.id, Some(data.timmy_local_user_view.person.id), ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; // Make sure block set the creator blocked assert!(read_comment_from_blocked_person.creator_blocked); + cleanup(data, pool).await + } + + #[tokio::test] + #[serial] + async fn test_liked_only() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Unblock sara first + let timmy_unblocks_sara_form = PersonBlockForm { + person_id: data.timmy_local_user_view.person.id, + target_id: data.inserted_sara_person.id, + }; + PersonBlock::unblock(pool, &timmy_unblocks_sara_form).await?; + + // Like a new comment + let comment_like_form = CommentLikeForm { + comment_id: data.inserted_comment_1.id, + post_id: data.inserted_post.id, + person_id: data.timmy_local_user_view.person.id, + score: 1, + }; + CommentLike::like(pool, &comment_like_form).await?; + let read_liked_comment_views = CommentQuery { local_user: (Some(&data.timmy_local_user_view)), liked_only: (true), ..Default::default() } .list(pool) - .await?; + .await? + .into_iter() + .map(|c| c.comment.content) + .collect::>(); - assert_eq!( - expected_comment_view_with_person, - read_liked_comment_views[0] - ); + // Shouldn't include your own post, only other peoples + assert_eq!(data.inserted_comment_1.content, read_liked_comment_views[0]); assert_length!(1, read_liked_comment_views); @@ -738,7 +765,7 @@ mod tests { async fn test_comment_tree() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; let top_path = data.inserted_comment_0.path.clone(); let read_comment_views_top_path = CommentQuery { @@ -811,7 +838,7 @@ mod tests { async fn test_languages() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; // by default, user has all languages enabled and should see all comments // (except from blocked user) @@ -826,7 +853,7 @@ mod tests { // change user lang to finnish, should only show one post in finnish and one undetermined let finnish_id = Language::read_id_from_code(pool, Some("fi")) .await? - .unwrap(); + .ok_or(LemmyErrorType::LanguageNotAllowed)?; LocalUserLanguage::update( pool, vec![finnish_id], @@ -846,7 +873,10 @@ mod tests { assert!(finnish_comment.is_some()); assert_eq!( data.inserted_comment_2.content, - finnish_comment.unwrap().comment.content + finnish_comment + .ok_or(LemmyErrorType::CouldntFindComment)? + .comment + .content ); // now show all comments with undetermined language (which is the default value) @@ -872,7 +902,7 @@ mod tests { async fn test_distinguished_first() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; let form = CommentUpdateForm { distinguished: Some(true), @@ -897,7 +927,7 @@ mod tests { async fn test_creator_is_moderator() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; // Make one of the inserted persons a moderator let person_id = data.inserted_sara_person.id; @@ -928,7 +958,7 @@ mod tests { async fn test_creator_is_admin() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; let comments = CommentQuery { sort: (Some(CommentSortType::Old)), @@ -953,7 +983,7 @@ mod tests { async fn test_saved_order() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; // Save two comments let save_comment_0_form = CommentSavedForm { @@ -1009,7 +1039,9 @@ mod tests { } async fn expected_comment_view(data: &Data, pool: &mut DbPool<'_>) -> LemmyResult { - let agg = CommentAggregates::read(pool, data.inserted_comment_0.id).await?; + let agg = CommentAggregates::read(pool, data.inserted_comment_0.id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; Ok(CommentView { creator_banned_from_community: false, banned_from_community: false, @@ -1127,7 +1159,7 @@ mod tests { async fn local_only_instance() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; Community::update( pool, @@ -1154,8 +1186,8 @@ mod tests { .await?; assert_eq!(5, authenticated_query.len()); - let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await; - assert!(unauthenticated_comment.is_err()); + let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await?; + assert!(unauthenticated_comment.is_none()); let authenticated_comment = CommentView::read( pool, @@ -1173,7 +1205,7 @@ mod tests { async fn comment_listing_local_user_banned_from_community() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; // Test that comment view shows if local user is blocked from community let banned_from_comm_person = PersonInsertForm::test_form(data.inserted_instance.id, "jill"); @@ -1202,7 +1234,8 @@ mod tests { data.inserted_comment_0.id, Some(inserted_banned_from_comm_local_user.person_id), ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; assert!(comment_view.banned_from_community); @@ -1215,14 +1248,15 @@ mod tests { async fn comment_listing_local_user_not_banned_from_community() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; let comment_view = CommentView::read( pool, data.inserted_comment_0.id, Some(data.timmy_local_user_view.person.id), ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; assert!(!comment_view.banned_from_community); diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 73310d743..e93c7409d 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -8,6 +8,8 @@ pub mod comment_view; #[cfg(feature = "full")] pub mod custom_emoji_view; #[cfg(feature = "full")] +pub mod local_image_view; +#[cfg(feature = "full")] pub mod local_user_view; #[cfg(feature = "full")] pub mod post_report_view; diff --git a/crates/db_views/src/local_image_view.rs b/crates/db_views/src/local_image_view.rs new file mode 100644 index 000000000..7b5b97095 --- /dev/null +++ b/crates/db_views/src/local_image_view.rs @@ -0,0 +1,61 @@ +use crate::structs::LocalImageView; +use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::LocalUserId, + schema::{local_image, local_user, person}, + utils::{get_conn, limit_and_offset, DbPool}, +}; + +impl LocalImageView { + async fn get_all_helper( + pool: &mut DbPool<'_>, + user_id: Option, + page: Option, + limit: Option, + ignore_page_limits: bool, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let mut query = local_image::table + .inner_join(local_user::table) + .inner_join(person::table.on(local_user::person_id.eq(person::id))) + .select((local_image::all_columns, person::all_columns)) + .order_by(local_image::published.desc()) + .into_boxed(); + + if let Some(user_id) = user_id { + query = query.filter(local_image::local_user_id.eq(user_id)) + } + + if !ignore_page_limits { + let (limit, offset) = limit_and_offset(page, limit)?; + query = query.limit(limit).offset(offset); + } + + query.load::(conn).await + } + + pub async fn get_all_paged_by_local_user_id( + pool: &mut DbPool<'_>, + user_id: LocalUserId, + page: Option, + limit: Option, + ) -> Result, Error> { + Self::get_all_helper(pool, Some(user_id), page, limit, false).await + } + + pub async fn get_all_by_local_user_id( + pool: &mut DbPool<'_>, + user_id: LocalUserId, + ) -> Result, Error> { + Self::get_all_helper(pool, Some(user_id), None, None, true).await + } + + pub async fn get_all( + pool: &mut DbPool<'_>, + page: Option, + limit: Option, + ) -> Result, Error> { + Self::get_all_helper(pool, None, page, limit, false).await + } +} diff --git a/crates/db_views/src/local_user_view.rs b/crates/db_views/src/local_user_view.rs index 96d6a3530..0c13b0a68 100644 --- a/crates/db_views/src/local_user_view.rs +++ b/crates/db_views/src/local_user_view.rs @@ -62,7 +62,7 @@ fn queries<'a>( .inner_join(local_user_vote_display_mode::table) .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))) .select(selection) - .first::(&mut conn) + .first(&mut conn) .await }; @@ -86,28 +86,37 @@ fn queries<'a>( } impl LocalUserView { - pub async fn read(pool: &mut DbPool<'_>, local_user_id: LocalUserId) -> Result { + pub async fn read( + pool: &mut DbPool<'_>, + local_user_id: LocalUserId, + ) -> Result, Error> { queries().read(pool, ReadBy::Id(local_user_id)).await } - pub async fn read_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { + pub async fn read_person( + pool: &mut DbPool<'_>, + person_id: PersonId, + ) -> Result, Error> { queries().read(pool, ReadBy::Person(person_id)).await } - pub async fn read_from_name(pool: &mut DbPool<'_>, name: &str) -> Result { + pub async fn read_from_name(pool: &mut DbPool<'_>, name: &str) -> Result, Error> { queries().read(pool, ReadBy::Name(name)).await } pub async fn find_by_email_or_name( pool: &mut DbPool<'_>, name_or_email: &str, - ) -> Result { + ) -> Result, Error> { queries() .read(pool, ReadBy::NameOrEmail(name_or_email)) .await } - pub async fn find_by_email(pool: &mut DbPool<'_>, from_email: &str) -> Result { + pub async fn find_by_email( + pool: &mut DbPool<'_>, + from_email: &str, + ) -> Result, Error> { queries().read(pool, ReadBy::Email(from_email)).await } diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index 878937d01..ac60deff9 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -163,7 +163,7 @@ fn queries<'a>() -> Queries< post_report::table.find(report_id).into_boxed(), my_person_id, ) - .first::(&mut conn) + .first(&mut conn) .await }; @@ -178,7 +178,8 @@ fn queries<'a>() -> Queries< query = query.filter(post::id.eq(post_id)); } - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest first (FIFO) + // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) if options.unresolved_only { query = query .filter(post_report::resolved.eq(false)) @@ -219,7 +220,7 @@ impl PostReportView { pool: &mut DbPool<'_>, report_id: PostReportId, my_person_id: PersonId, - ) -> Result { + ) -> Result, Error> { queries().read(pool, (report_id, my_person_id)).await } @@ -421,6 +422,7 @@ mod tests { let read_jessica_report_view = PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) .await + .unwrap() .unwrap(); assert_eq!( @@ -458,6 +460,7 @@ mod tests { let read_jessica_report_view_after_resolve = PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) .await + .unwrap() .unwrap(); assert!(read_jessica_report_view_after_resolve.post_report.resolved); assert_eq!( diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index b323437d5..afb0f435f 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -305,7 +305,7 @@ fn queries<'a>() -> Queries< Commented::new(query) .text("PostView::read") - .first::(&mut conn) + .first(&mut conn) .await }; @@ -396,11 +396,13 @@ fn queries<'a>() -> Queries< if let Some(search_term) = &options.search_term { let searcher = fuzzy_search(search_term); - query = query.filter( - post::name - .ilike(searcher.clone()) - .or(post::body.ilike(searcher)), - ); + query = query + .filter( + post::name + .ilike(searcher.clone()) + .or(post::body.ilike(searcher)), + ) + .filter(not(post::removed.or(post::deleted))); } // If there is a content warning, show nsfw content by default. @@ -450,11 +452,12 @@ fn queries<'a>() -> Queries< } } - if let Some(person_id) = my_person_id { + if let Some(my_id) = my_person_id { + let not_creator_filter = post_aggregates::creator_id.ne(my_id); if options.liked_only { - query = query.filter(score(person_id).eq(1)); + query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); } else if options.disliked_only { - query = query.filter(score(person_id).eq(-1)); + query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); } }; @@ -581,12 +584,10 @@ impl PostView { post_id: PostId, my_person_id: Option, is_mod_or_admin: bool, - ) -> Result { - let res = queries() + ) -> Result, Error> { + queries() .read(pool, (post_id, my_person_id, is_mod_or_admin)) - .await?; - - Ok(res) + .await } } @@ -597,24 +598,27 @@ impl PaginationCursor { PaginationCursor(format!("P{:x}", view.counts.post_id.0)) } pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { - Ok(PaginationCursorData( - PostAggregates::read( - pool, - PostId( - self - .0 - .get(1..) - .and_then(|e| i32::from_str_radix(e, 16).ok()) - .ok_or_else(|| Error::QueryBuilderError("Could not parse pagination token".into()))?, - ), - ) - .await?, - )) + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let token = PostAggregates::read( + pool, + PostId( + self + .0 + .get(1..) + .and_then(|e| i32::from_str_radix(e, 16).ok()) + .ok_or_else(err_msg)?, + ), + ) + .await? + .ok_or_else(err_msg)?; + + Ok(PaginationCursorData(token)) } } // currently we use a postaggregates struct as the pagination token. -// we only use some of the properties of the post aggregates, depending on which sort type we page by +// we only use some of the properties of the post aggregates, depending on which sort type we page +// by #[derive(Clone)] pub struct PaginationCursorData(PostAggregates); @@ -624,7 +628,8 @@ pub struct PostQuery<'a> { pub sort: Option, pub creator_id: Option, pub community_id: Option, - // if true, the query should be handled as if community_id was not given except adding the literal filter + // if true, the query should be handled as if community_id was not given except adding the + // literal filter pub community_id_just_for_prefetch: bool, pub local_user: Option<&'a LocalUserView>, pub search_term: Option, @@ -646,15 +651,17 @@ impl<'a> PostQuery<'a> { site: &Site, pool: &mut DbPool<'_>, ) -> Result>, Error> { - // first get one page for the most popular community to get an upper bound for the the page end for the real query - // the reason this is needed is that when fetching posts for a single community PostgreSQL can optimize - // the query to use an index on e.g. (=, >=, >=, >=) and fetch only LIMIT rows - // but for the followed-communities query it has to query the index on (IN, >=, >=, >=) - // which it currently can't do at all (as of PG 16). see the discussion here: - // https://github.com/LemmyNet/lemmy/issues/2877#issuecomment-1673597190 + // first get one page for the most popular community to get an upper bound for the page end for + // the real query. the reason this is needed is that when fetching posts for a single + // community PostgreSQL can optimize the query to use an index on e.g. (=, >=, >=, >=) and + // fetch only LIMIT rows but for the followed-communities query it has to query the index on + // (IN, >=, >=, >=) which it currently can't do at all (as of PG 16). see the discussion + // here: https://github.com/LemmyNet/lemmy/issues/2877#issuecomment-1673597190 // - // the results are correct no matter which community we fetch these for, since it basically covers the "worst case" of the whole page consisting of posts from one community - // but using the largest community decreases the pagination-frame so make the real query more efficient. + // the results are correct no matter which community we fetch these for, since it basically + // covers the "worst case" of the whole page consisting of posts from one community + // but using the largest community decreases the pagination-frame so make the real query more + // efficient. use lemmy_db_schema::schema::{ community_aggregates::dsl::{community_aggregates, community_id, users_active_month}, community_follower::dsl::{ @@ -705,7 +712,8 @@ impl<'a> PostQuery<'a> { ) .await?; // take last element of array. if this query returned less than LIMIT elements, - // the heuristic is invalid since we can't guarantee the full query will return >= LIMIT results (return original query) + // the heuristic is invalid since we can't guarantee the full query will return >= LIMIT results + // (return original query) if (v.len() as i64) < limit { Ok(Some(self.clone())) } else { @@ -783,7 +791,7 @@ mod tests { SortType, SubscribedType, }; - use lemmy_utils::error::LemmyResult; + use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use pretty_assertions::assert_eq; use serial_test::serial; use std::{collections::HashSet, time::Duration}; @@ -947,9 +955,8 @@ mod tests { show_bot_accounts: Some(false), ..Default::default() }; - let inserted_local_user = - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user = inserted_local_user; + LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; + data.local_user_view.local_user.show_bot_accounts = false; let read_post_listing = PostQuery { community_id: Some(data.inserted_community.id), @@ -964,7 +971,8 @@ mod tests { Some(data.local_user_view.person.id), false, ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; let expected_post_listing_with_user = expected_post_view(&data, pool).await?; @@ -982,9 +990,8 @@ mod tests { show_bot_accounts: Some(true), ..Default::default() }; - let inserted_local_user = - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user = inserted_local_user; + LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; + data.local_user_view.local_user.show_bot_accounts = true; let post_listings_with_bots = PostQuery { community_id: Some(data.inserted_community.id), @@ -1014,7 +1021,9 @@ mod tests { .await?; let read_post_listing_single_no_person = - PostView::read(pool, data.inserted_post.id, None, false).await?; + PostView::read(pool, data.inserted_post.id, None, false) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; let expected_post_listing_no_person = expected_post_view(&data, pool).await?; @@ -1091,7 +1100,8 @@ mod tests { Some(data.local_user_view.person.id), false, ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; let mut expected_post_with_upvote = expected_post_view(&data, pool).await?; expected_post_with_upvote.my_vote = Some(1); @@ -1103,9 +1113,8 @@ mod tests { show_bot_accounts: Some(false), ..Default::default() }; - let inserted_local_user = - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user = inserted_local_user; + LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; + data.local_user_view.local_user.show_bot_accounts = false; let read_post_listing = PostQuery { community_id: Some(data.inserted_community.id), @@ -1115,6 +1124,36 @@ mod tests { .await?; assert_eq!(vec![expected_post_with_upvote], read_post_listing); + let like_removed = + PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id).await?; + assert_eq!(1, like_removed); + cleanup(data, pool).await + } + + #[tokio::test] + #[serial] + async fn post_listing_liked_only() -> LemmyResult<()> { + let pool = &build_db_pool().await?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Like both the bot post, and your own + // The liked_only should not show your own post + let post_like_form = PostLikeForm { + post_id: data.inserted_post.id, + person_id: data.local_user_view.person.id, + score: 1, + }; + PostLike::like(pool, &post_like_form).await?; + + let bot_post_like_form = PostLikeForm { + post_id: data.inserted_bot_post.id, + person_id: data.local_user_view.person.id, + score: 1, + }; + PostLike::like(pool, &bot_post_like_form).await?; + + // Read the liked only let read_liked_post_listing = PostQuery { community_id: Some(data.inserted_community.id), liked_only: true, @@ -1122,7 +1161,9 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(read_post_listing, read_liked_post_listing); + + // This should only include the bot post, not the one you created + assert_eq!(vec![POST_BY_BOT], names(&read_liked_post_listing)); let read_disliked_post_listing = PostQuery { community_id: Some(data.inserted_community.id), @@ -1131,11 +1172,10 @@ mod tests { } .list(&data.site, pool) .await?; + + // Should be no posts assert_eq!(read_disliked_post_listing, vec![]); - let like_removed = - PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id).await?; - assert_eq!(1, like_removed); cleanup(data, pool).await } @@ -1394,7 +1434,8 @@ mod tests { let mut inserted_post_ids = vec![]; let mut inserted_comment_ids = vec![]; - // Create 150 posts with varying non-correlating values for publish date, number of comments, and featured + // Create 150 posts with varying non-correlating values for publish date, number of comments, + // and featured for comments in 0..10 { for _ in 0..15 { let post_form = PostInsertForm::builder() @@ -1495,9 +1536,8 @@ mod tests { show_read_posts: Some(false), ..Default::default() }; - let inserted_local_user = - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user = inserted_local_user; + LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; + data.local_user_view.local_user.show_read_posts = false; // Mark a post as read PostRead::mark_as_read( @@ -1548,7 +1588,7 @@ mod tests { assert!( &post_listings_show_hidden .first() - .expect("first post should exist") + .ok_or(LemmyErrorType::CouldntFindPost)? .hidden ); @@ -1573,7 +1613,9 @@ mod tests { &data.inserted_community, &data.inserted_post, ); - let agg = PostAggregates::read(pool, inserted_post.id).await?; + let agg = PostAggregates::read(pool, inserted_post.id) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; Ok(PostView { post: Post { @@ -1716,8 +1758,8 @@ mod tests { .await?; assert_eq!(2, authenticated_query.len()); - let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await; - assert!(unauthenticated_post.is_err()); + let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await?; + assert!(unauthenticated_post.is_none()); let authenticated_post = PostView::read( pool, @@ -1767,7 +1809,8 @@ mod tests { Some(inserted_banned_from_comm_local_user.person_id), false, ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; assert!(post_view.banned_from_community); @@ -1788,7 +1831,8 @@ mod tests { Some(data.local_user_view.person.id), false, ) - .await?; + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; assert!(!post_view.banned_from_community); diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index dff9820d9..6011574e6 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -42,14 +42,15 @@ fn queries<'a>() -> Queries< let read = move |mut conn: DbConn<'a>, report_id: PrivateMessageReportId| async move { all_joins(private_message_report::table.find(report_id).into_boxed()) - .first::(&mut conn) + .first(&mut conn) .await }; let list = move |mut conn: DbConn<'a>, options: PrivateMessageReportQuery| async move { let mut query = all_joins(private_message_report::table.into_boxed()); - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest first (FIFO) + // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) if options.unresolved_only { query = query .filter(private_message_report::resolved.eq(false)) @@ -77,7 +78,7 @@ impl PrivateMessageReportView { pub async fn read( pool: &mut DbPool<'_>, report_id: PrivateMessageReportId, - ) -> Result { + ) -> Result, Error> { queries().read(pool, report_id).await } diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 466f3bdf0..764ef1dcb 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -53,7 +53,7 @@ fn queries<'a>() -> Queries< all_joins(private_message::table.find(private_message_id).into_boxed()) .order_by(private_message::published.desc()) .select(selection) - .first::(&mut conn) + .first(&mut conn) .await }; @@ -113,7 +113,7 @@ impl PrivateMessageView { pub async fn read( pool: &mut DbPool<'_>, private_message_id: PrivateMessageId, - ) -> Result { + ) -> Result, Error> { queries().read(pool, private_message_id).await } diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index 85916bb90..65629d65c 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -42,14 +42,15 @@ fn queries<'a>() -> Queries< .find(registration_application_id) .into_boxed(), ) - .first::(&mut conn) + .first(&mut conn) .await }; let list = move |mut conn: DbConn<'a>, options: RegistrationApplicationQuery| async move { let mut query = all_joins(registration_application::table.into_boxed()); - // If viewing all applications, order by newest, but if viewing unresolved only, show the oldest first (FIFO) + // If viewing all applications, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) if options.unread_only { query = query .filter(registration_application::admin_id.is_null()) @@ -76,7 +77,7 @@ impl RegistrationApplicationView { pub async fn read( pool: &mut DbPool<'_>, registration_application_id: i32, - ) -> Result { + ) -> Result, Error> { queries().read(pool, registration_application_id).await } @@ -209,6 +210,7 @@ mod tests { let read_sara_app_view = RegistrationApplicationView::read(pool, sara_app.id) .await + .unwrap() .unwrap(); let jess_person_form = PersonInsertForm::builder() @@ -240,6 +242,7 @@ mod tests { let read_jess_app_view = RegistrationApplicationView::read(pool, jess_app.id) .await + .unwrap() .unwrap(); let mut expected_sara_app_view = RegistrationApplicationView { @@ -343,6 +346,7 @@ mod tests { let read_sara_app_view_after_approve = RegistrationApplicationView::read(pool, sara_app.id) .await + .unwrap() .unwrap(); // Make sure the columns changed diff --git a/crates/db_views/src/site_view.rs b/crates/db_views/src/site_view.rs index 52074c982..8f0722318 100644 --- a/crates/db_views/src/site_view.rs +++ b/crates/db_views/src/site_view.rs @@ -1,5 +1,5 @@ use crate::structs::SiteView; -use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{result::Error, ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ schema::{local_site, local_site_rate_limit, site, site_aggregates}, @@ -7,7 +7,7 @@ use lemmy_db_schema::{ }; impl SiteView { - pub async fn read_local(pool: &mut DbPool<'_>) -> Result { + pub async fn read_local(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; site::table .inner_join(local_site::table) @@ -21,7 +21,8 @@ impl SiteView { local_site_rate_limit::all_columns, site_aggregates::all_columns, )) - .first::(conn) + .first(conn) .await + .optional() } } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index a290ca4a1..350e4cad4 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -8,6 +8,7 @@ use lemmy_db_schema::{ community::Community, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, + images::LocalImage, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, @@ -111,9 +112,10 @@ pub struct PostReportView { pub resolver: Option, } -/// currently this is just a wrapper around post id, but should be seen as opaque from the client's perspective -/// stringified since we might want to use arbitrary info later, with a P prepended to prevent ossification -/// (api users love to make assumptions (e.g. parse stuff that looks like numbers as numbers) about apis that aren't part of the spec +/// currently this is just a wrapper around post id, but should be seen as opaque from the client's +/// perspective. stringified since we might want to use arbitrary info later, with a P prepended to +/// prevent ossification (api users love to make assumptions (e.g. parse stuff that looks like +/// numbers as numbers) about apis that aren't part of the spec #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(ts_rs::TS))] #[cfg_attr(feature = "full", ts(export))] @@ -214,3 +216,14 @@ pub struct VoteView { pub creator_banned_from_community: bool, pub score: i16, } + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A local image view. +pub struct LocalImageView { + pub local_image: LocalImage, + pub person: Person, +} diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 77f744a97..547c00e53 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -1,6 +1,6 @@ use crate::structs::CommentReplyView; use diesel::{ - dsl::exists, + dsl::{exists, not}, pg::Pg, result::Error, sql_types, @@ -188,7 +188,7 @@ fn queries<'a>() -> Queries< comment_reply::table.find(comment_reply_id).into_boxed(), my_person_id, ) - .first::(&mut conn) + .first(&mut conn) .await }; @@ -217,6 +217,11 @@ fn queries<'a>() -> Queries< CommentSortType::Top => query.order_by(comment_aggregates::score.desc()), }; + // Don't show replies from blocked persons + if let Some(my_person_id) = options.my_person_id { + query = query.filter(not(is_creator_blocked(my_person_id))); + } + let (limit, offset) = limit_and_offset(options.page, options.limit)?; query @@ -234,7 +239,7 @@ impl CommentReplyView { pool: &mut DbPool<'_>, comment_reply_id: CommentReplyId, my_person_id: Option, - ) -> Result { + ) -> Result, Error> { queries().read(pool, (comment_reply_id, my_person_id)).await } @@ -268,7 +273,7 @@ impl CommentReplyView { } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct CommentReplyQuery { pub my_person_id: Option, pub recipient_id: Option, @@ -284,3 +289,141 @@ impl CommentReplyQuery { queries().list(pool, self).await } } + +#[cfg(test)] +#[allow(clippy::indexing_slicing)] +mod tests { + + use crate::{comment_reply_view::CommentReplyQuery, structs::CommentReplyView}; + use lemmy_db_schema::{ + source::{ + comment::{Comment, CommentInsertForm}, + comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + person::{Person, PersonInsertForm}, + person_block::{PersonBlock, PersonBlockForm}, + post::{Post, PostInsertForm}, + }, + traits::{Blockable, Crud}, + 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() -> 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 terry_form = PersonInsertForm::builder() + .name("terrylake".into()) + .public_key("pubkey".to_string()) + .instance_id(inserted_instance.id) + .build(); + let inserted_terry = Person::create(pool, &terry_form).await?; + + let recipient_form = PersonInsertForm::builder() + .name("terrylakes recipient".into()) + .public_key("pubkey".to_string()) + .instance_id(inserted_instance.id) + .build(); + + let inserted_recipient = Person::create(pool, &recipient_form).await?; + let recipient_id = inserted_recipient.id; + + let new_community = CommunityInsertForm::builder() + .name("test community lake".to_string()) + .title("nada".to_owned()) + .public_key("pubkey".to_string()) + .instance_id(inserted_instance.id) + .build(); + + let inserted_community = Community::create(pool, &new_community).await?; + + let new_post = PostInsertForm::builder() + .name("A test post".into()) + .creator_id(inserted_terry.id) + .community_id(inserted_community.id) + .build(); + + let inserted_post = Post::create(pool, &new_post).await?; + + let comment_form = CommentInsertForm::builder() + .content("A test comment".into()) + .creator_id(inserted_terry.id) + .post_id(inserted_post.id) + .build(); + + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + let comment_reply_form = CommentReplyInsertForm { + recipient_id: inserted_recipient.id, + comment_id: inserted_comment.id, + read: None, + }; + + let inserted_reply = CommentReply::create(pool, &comment_reply_form).await?; + + let expected_reply = CommentReply { + id: inserted_reply.id, + recipient_id: inserted_reply.recipient_id, + comment_id: inserted_reply.comment_id, + read: false, + published: inserted_reply.published, + }; + + let read_reply = CommentReply::read(pool, inserted_reply.id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; + + let comment_reply_update_form = CommentReplyUpdateForm { read: Some(false) }; + let updated_reply = + CommentReply::update(pool, inserted_reply.id, &comment_reply_update_form).await?; + + // Test to make sure counts and blocks work correctly + let unread_replies = CommentReplyView::get_unread_replies(pool, recipient_id).await?; + + let query = CommentReplyQuery { + recipient_id: Some(recipient_id), + my_person_id: Some(recipient_id), + sort: None, + unread_only: false, + show_bot_accounts: true, + page: None, + limit: None, + }; + let replies = query.clone().list(pool).await?; + assert_eq!(1, unread_replies); + assert_eq!(1, replies.len()); + + // Block the person, and make sure these counts are now empty + let block_form = PersonBlockForm { + person_id: recipient_id, + target_id: inserted_terry.id, + }; + PersonBlock::block(pool, &block_form).await?; + + let unread_replies_after_block = + CommentReplyView::get_unread_replies(pool, recipient_id).await?; + let replies_after_block = query.list(pool).await?; + assert_eq!(0, unread_replies_after_block); + assert_eq!(0, replies_after_block.len()); + + Comment::delete(pool, inserted_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_terry.id).await?; + Person::delete(pool, inserted_recipient.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + assert_eq!(expected_reply, read_reply); + assert_eq!(expected_reply, inserted_reply); + assert_eq!(expected_reply, updated_reply); + Ok(()) + } +} diff --git a/crates/db_views_actor/src/community_follower_view.rs b/crates/db_views_actor/src/community_follower_view.rs index b2eeda29e..7b942e043 100644 --- a/crates/db_views_actor/src/community_follower_view.rs +++ b/crates/db_views_actor/src/community_follower_view.rs @@ -14,7 +14,8 @@ use lemmy_db_schema::{ }; impl CommunityFollowerView { - /// return a list of local community ids and remote inboxes that at least one user of the given instance has followed + /// return a list of local community ids and remote inboxes that at least one user of the given + /// instance has followed pub async fn get_instance_followed_community_inboxes( pool: &mut DbPool<'_>, instance_id: InstanceId, @@ -22,8 +23,9 @@ impl CommunityFollowerView { ) -> Result, Error> { let conn = &mut get_conn(pool).await?; // In most cases this will fetch the same url many times (the shared inbox url) - // PG will only send a single copy to rust, but it has to scan through all follower rows (same as it was before). - // So on the PG side it would be possible to optimize this further by adding e.g. a new table community_followed_instances (community_id, instance_id) + // PG will only send a single copy to rust, but it has to scan through all follower rows (same + // as it was before). So on the PG side it would be possible to optimize this further by + // adding e.g. a new table community_followed_instances (community_id, instance_id) // that would work for all instances that support fully shared inboxes. // It would be a bit more complicated though to keep it in sync. @@ -31,7 +33,8 @@ impl CommunityFollowerView { .inner_join(community::table) .inner_join(person::table) .filter(person::instance_id.eq(instance_id)) - .filter(community::local) // this should be a no-op since community_followers table only has local-person+remote-community or remote-person+local-community + .filter(community::local) // this should be a no-op since community_followers table only has + // local-person+remote-community or remote-person+local-community .filter(not(person::local)) .filter(community_follower::published.gt(published_since.naive_utc())) .select(( diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 3107478dc..0ff421540 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -102,7 +102,7 @@ fn queries<'a>() -> Queries< query = query.filter(community::visibility.eq(CommunityVisibility::Public)); } - query.first::(&mut conn).await + query.first(&mut conn).await }; let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { @@ -154,7 +154,7 @@ fn queries<'a>() -> Queries< if let Some(listing_type) = options.listing_type { query = match listing_type { - ListingType::Subscribed => query.filter(community_follower::pending.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)), + ListingType::Subscribed => query.filter(community_follower::pending.is_not_null()), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ ListingType::Local => query.filter(community::local.eq(true)), _ => query, }; @@ -194,7 +194,7 @@ impl CommunityView { community_id: CommunityId, my_person_id: Option, is_mod_or_admin: bool, - ) -> Result { + ) -> Result, Error> { queries() .read(pool, (community_id, my_person_id, is_mod_or_admin)) .await @@ -209,9 +209,10 @@ impl CommunityView { CommunityModeratorView::is_community_moderator(pool, community_id, person_id).await?; if is_mod { Ok(true) + } else if let Ok(Some(person_view)) = PersonView::read(pool, person_id).await { + Ok(person_view.is_admin) } else { - let is_admin = PersonView::read(pool, person_id).await?.is_admin; - Ok(is_admin) + Ok(false) } } @@ -223,11 +224,12 @@ impl CommunityView { let is_mod_of_any = CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await?; if is_mod_of_any { - return Ok(true); + Ok(true) + } else if let Ok(Some(person_view)) = PersonView::read(pool, person_id).await { + Ok(person_view.is_admin) + } else { + Ok(false) } - - let is_admin = PersonView::read(pool, person_id).await?.is_admin; - Ok(is_admin) } } @@ -384,8 +386,10 @@ mod tests { assert_eq!(1, authenticated_query.len()); let unauthenticated_community = - CommunityView::read(pool, data.inserted_community.id, None, false).await; - assert!(unauthenticated_community.is_err()); + CommunityView::read(pool, data.inserted_community.id, None, false) + .await + .unwrap(); + assert!(unauthenticated_community.is_none()); let authenticated_community = CommunityView::read( pool, diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index 399850292..d42987a68 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -1,6 +1,6 @@ use crate::structs::PersonMentionView; use diesel::{ - dsl::exists, + dsl::{exists, not}, pg::Pg, result::Error, sql_types, @@ -187,7 +187,7 @@ fn queries<'a>() -> Queries< person_mention::table.find(person_mention_id).into_boxed(), my_person_id, ) - .first::(&mut conn) + .first(&mut conn) .await }; @@ -216,6 +216,11 @@ fn queries<'a>() -> Queries< CommentSortType::Top => query.order_by(comment_aggregates::score.desc()), }; + // Don't show mentions from blocked persons + if let Some(my_person_id) = options.my_person_id { + query = query.filter(not(is_creator_blocked(my_person_id))); + } + let (limit, offset) = limit_and_offset(options.page, options.limit)?; query @@ -233,7 +238,7 @@ impl PersonMentionView { pool: &mut DbPool<'_>, person_mention_id: PersonMentionId, my_person_id: Option, - ) -> Result { + ) -> Result, Error> { queries() .read(pool, (person_mention_id, my_person_id)) .await @@ -249,6 +254,15 @@ impl PersonMentionView { person_mention::table .inner_join(comment::table) + .left_join( + person_block::table.on( + comment::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(my_person_id)), + ), + ) + // Dont count replies from blocked users + .filter(person_block::person_id.is_null()) .filter(person_mention::recipient_id.eq(my_person_id)) .filter(person_mention::read.eq(false)) .filter(comment::deleted.eq(false)) @@ -259,7 +273,7 @@ impl PersonMentionView { } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct PersonMentionQuery { pub my_person_id: Option, pub recipient_id: Option, @@ -275,3 +289,143 @@ impl PersonMentionQuery { queries().list(pool, self).await } } + +#[cfg(test)] +#[allow(clippy::indexing_slicing)] +mod tests { + + use crate::{person_mention_view::PersonMentionQuery, structs::PersonMentionView}; + use lemmy_db_schema::{ + source::{ + comment::{Comment, CommentInsertForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + person::{Person, PersonInsertForm}, + person_block::{PersonBlock, PersonBlockForm}, + person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm}, + post::{Post, PostInsertForm}, + }, + traits::{Blockable, Crud}, + 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() -> 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 new_person = PersonInsertForm::builder() + .name("terrylake".into()) + .public_key("pubkey".to_string()) + .instance_id(inserted_instance.id) + .build(); + + let inserted_person = Person::create(pool, &new_person).await?; + + let recipient_form = PersonInsertForm::builder() + .name("terrylakes recipient".into()) + .public_key("pubkey".to_string()) + .instance_id(inserted_instance.id) + .build(); + + let inserted_recipient = Person::create(pool, &recipient_form).await?; + let recipient_id = inserted_recipient.id; + + let new_community = CommunityInsertForm::builder() + .name("test community lake".to_string()) + .title("nada".to_owned()) + .public_key("pubkey".to_string()) + .instance_id(inserted_instance.id) + .build(); + + let inserted_community = Community::create(pool, &new_community).await?; + + let new_post = PostInsertForm::builder() + .name("A test post".into()) + .creator_id(inserted_person.id) + .community_id(inserted_community.id) + .build(); + + let inserted_post = Post::create(pool, &new_post).await?; + + let comment_form = CommentInsertForm::builder() + .content("A test comment".into()) + .creator_id(inserted_person.id) + .post_id(inserted_post.id) + .build(); + + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + let person_mention_form = PersonMentionInsertForm { + recipient_id: inserted_recipient.id, + comment_id: inserted_comment.id, + read: None, + }; + + let inserted_mention = PersonMention::create(pool, &person_mention_form).await?; + + let expected_mention = PersonMention { + id: inserted_mention.id, + recipient_id: inserted_mention.recipient_id, + comment_id: inserted_mention.comment_id, + read: false, + published: inserted_mention.published, + }; + + let read_mention = PersonMention::read(pool, inserted_mention.id) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; + + let person_mention_update_form = PersonMentionUpdateForm { read: Some(false) }; + let updated_mention = + PersonMention::update(pool, inserted_mention.id, &person_mention_update_form).await?; + + // Test to make sure counts and blocks work correctly + let unread_mentions = PersonMentionView::get_unread_mentions(pool, recipient_id).await?; + + let query = PersonMentionQuery { + recipient_id: Some(recipient_id), + my_person_id: Some(recipient_id), + sort: None, + unread_only: false, + show_bot_accounts: true, + page: None, + limit: None, + }; + let mentions = query.clone().list(pool).await?; + assert_eq!(1, unread_mentions); + assert_eq!(1, mentions.len()); + + // Block the person, and make sure these counts are now empty + let block_form = PersonBlockForm { + person_id: recipient_id, + target_id: inserted_person.id, + }; + PersonBlock::block(pool, &block_form).await?; + + let unread_mentions_after_block = + PersonMentionView::get_unread_mentions(pool, recipient_id).await?; + let mentions_after_block = query.list(pool).await?; + assert_eq!(0, unread_mentions_after_block); + assert_eq!(0, mentions_after_block.len()); + + Comment::delete(pool, inserted_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_person.id).await?; + Person::delete(pool, inserted_recipient.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + assert_eq!(expected_mention, read_mention); + assert_eq!(expected_mention, inserted_mention); + assert_eq!(expected_mention, updated_mention); + + Ok(()) + } +} diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index b3eac296d..5734bc812 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -72,7 +72,7 @@ fn queries<'a>( let read = move |mut conn: DbConn<'a>, person_id: PersonId| async move { all_joins(person::table.find(person_id).into_boxed()) - .first::(&mut conn) + .first(&mut conn) .await }; @@ -134,7 +134,7 @@ fn queries<'a>( } impl PersonView { - pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { + pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { queries().read(pool, person_id).await } @@ -163,12 +163,10 @@ impl PersonQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { use super::*; - use diesel::NotFound; use lemmy_db_schema::{ assert_length, source::{ @@ -179,7 +177,7 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; - use lemmy_utils::error::LemmyResult; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use pretty_assertions::assert_eq; use serial_test::serial; @@ -254,8 +252,8 @@ mod tests { ) .await?; - let read = PersonView::read(pool, data.alice.id).await; - assert_eq!(read.err(), Some(NotFound)); + let read = PersonView::read(pool, data.alice.id).await?; + assert!(read.is_none()); let list = PersonQuery { sort: Some(SortType::New), @@ -314,10 +312,16 @@ mod tests { assert_length!(1, list); assert_eq!(list[0].person.id, data.alice.id); - let is_admin = PersonView::read(pool, data.alice.id).await?.is_admin; + let is_admin = PersonView::read(pool, data.alice.id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)? + .is_admin; assert!(is_admin); - let is_admin = PersonView::read(pool, data.bob.id).await?.is_admin; + let is_admin = PersonView::read(pool, data.bob.id) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)? + .is_admin; assert!(!is_admin); cleanup(data, pool).await diff --git a/crates/db_views_moderator/src/admin_purge_comment_view.rs b/crates/db_views_moderator/src/admin_purge_comment_view.rs index f62fe0f22..4c650b6fa 100644 --- a/crates/db_views_moderator/src/admin_purge_comment_view.rs +++ b/crates/db_views_moderator/src/admin_purge_comment_view.rs @@ -40,6 +40,11 @@ impl AdminPurgeCommentView { query = query.filter(admin_purge_comment::admin_person_id.eq(admin_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/admin_purge_community_view.rs b/crates/db_views_moderator/src/admin_purge_community_view.rs index 23967ee3b..5eadb8985 100644 --- a/crates/db_views_moderator/src/admin_purge_community_view.rs +++ b/crates/db_views_moderator/src/admin_purge_community_view.rs @@ -38,6 +38,11 @@ impl AdminPurgeCommunityView { query = query.filter(admin_purge_community::admin_person_id.eq(admin_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/admin_purge_person_view.rs b/crates/db_views_moderator/src/admin_purge_person_view.rs index 097785d25..b6dd834c5 100644 --- a/crates/db_views_moderator/src/admin_purge_person_view.rs +++ b/crates/db_views_moderator/src/admin_purge_person_view.rs @@ -38,6 +38,11 @@ impl AdminPurgePersonView { query = query.filter(admin_purge_person::admin_person_id.eq(admin_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/admin_purge_post_view.rs b/crates/db_views_moderator/src/admin_purge_post_view.rs index 8f5eb3a14..b77493c25 100644 --- a/crates/db_views_moderator/src/admin_purge_post_view.rs +++ b/crates/db_views_moderator/src/admin_purge_post_view.rs @@ -40,6 +40,11 @@ impl AdminPurgePostView { query = query.filter(admin_purge_post::admin_person_id.eq(admin_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_add_community_view.rs b/crates/db_views_moderator/src/mod_add_community_view.rs index f96a9b80b..1068aba75 100644 --- a/crates/db_views_moderator/src/mod_add_community_view.rs +++ b/crates/db_views_moderator/src/mod_add_community_view.rs @@ -52,6 +52,11 @@ impl ModAddCommunityView { query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_add_view.rs b/crates/db_views_moderator/src/mod_add_view.rs index 28fb0a2b6..c5612c4ad 100644 --- a/crates/db_views_moderator/src/mod_add_view.rs +++ b/crates/db_views_moderator/src/mod_add_view.rs @@ -44,6 +44,11 @@ impl ModAddView { query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_ban_from_community_view.rs b/crates/db_views_moderator/src/mod_ban_from_community_view.rs index 02f18099c..d2d6038f3 100644 --- a/crates/db_views_moderator/src/mod_ban_from_community_view.rs +++ b/crates/db_views_moderator/src/mod_ban_from_community_view.rs @@ -54,6 +54,11 @@ impl ModBanFromCommunityView { query = query.filter(mod_ban_from_community::other_person_id.eq(other_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_ban_view.rs b/crates/db_views_moderator/src/mod_ban_view.rs index 94ac360db..ca0723e83 100644 --- a/crates/db_views_moderator/src/mod_ban_view.rs +++ b/crates/db_views_moderator/src/mod_ban_view.rs @@ -44,6 +44,11 @@ impl ModBanView { query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_feature_post_view.rs b/crates/db_views_moderator/src/mod_feature_post_view.rs index 1bf83688d..4c0fdb4f7 100644 --- a/crates/db_views_moderator/src/mod_feature_post_view.rs +++ b/crates/db_views_moderator/src/mod_feature_post_view.rs @@ -55,6 +55,11 @@ impl ModFeaturePostView { query = query.filter(post::id.eq(post_id)); } + // If a comment ID is given, then don't find any results + if params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_hide_community_view.rs b/crates/db_views_moderator/src/mod_hide_community_view.rs index 36b549814..3c8a7e627 100644 --- a/crates/db_views_moderator/src/mod_hide_community_view.rs +++ b/crates/db_views_moderator/src/mod_hide_community_view.rs @@ -45,6 +45,11 @@ impl ModHideCommunityView { query = query.filter(mod_hide_community::mod_person_id.eq(admin_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_lock_post_view.rs b/crates/db_views_moderator/src/mod_lock_post_view.rs index 6f7e83ec7..5a6c753d9 100644 --- a/crates/db_views_moderator/src/mod_lock_post_view.rs +++ b/crates/db_views_moderator/src/mod_lock_post_view.rs @@ -56,6 +56,11 @@ impl ModLockPostView { query = query.filter(post::id.eq(post_id)); } + // If a comment ID is given, then don't find any results + if params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_remove_comment_view.rs b/crates/db_views_moderator/src/mod_remove_comment_view.rs index e7d695a5c..cf0ed325c 100644 --- a/crates/db_views_moderator/src/mod_remove_comment_view.rs +++ b/crates/db_views_moderator/src/mod_remove_comment_view.rs @@ -58,6 +58,11 @@ impl ModRemoveCommentView { query = query.filter(comment::id.eq(comment_id)); } + // If a post ID is given, then don't find any results + if params.post_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_remove_community_view.rs b/crates/db_views_moderator/src/mod_remove_community_view.rs index 2bc92acc8..ac620ebdb 100644 --- a/crates/db_views_moderator/src/mod_remove_community_view.rs +++ b/crates/db_views_moderator/src/mod_remove_community_view.rs @@ -39,6 +39,11 @@ impl ModRemoveCommunityView { query = query.filter(mod_remove_community::mod_person_id.eq(mod_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_remove_post_view.rs b/crates/db_views_moderator/src/mod_remove_post_view.rs index fb0b3e6c1..98504a8e7 100644 --- a/crates/db_views_moderator/src/mod_remove_post_view.rs +++ b/crates/db_views_moderator/src/mod_remove_post_view.rs @@ -56,6 +56,11 @@ impl ModRemovePostView { query = query.filter(post::id.eq(post_id)); } + // If a comment ID is given, then don't find any results + if params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/db_views_moderator/src/mod_transfer_community_view.rs b/crates/db_views_moderator/src/mod_transfer_community_view.rs index 3d48b0f67..6d62d347a 100644 --- a/crates/db_views_moderator/src/mod_transfer_community_view.rs +++ b/crates/db_views_moderator/src/mod_transfer_community_view.rs @@ -54,6 +54,11 @@ impl ModTransferCommunityView { query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); }; + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + let (limit, offset) = limit_and_offset(params.page, params.limit)?; query diff --git a/crates/federate/Cargo.toml b/crates/federate/Cargo.toml index 9ccd8adc3..2405d3af0 100644 --- a/crates/federate/Cargo.toml +++ b/crates/federate/Cargo.toml @@ -19,6 +19,7 @@ lemmy_api_common.workspace = true lemmy_apub.workspace = true lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_views_actor.workspace = true +lemmy_utils.workspace = true activitypub_federation.workspace = true anyhow.workspace = true @@ -32,4 +33,7 @@ serde_json.workspace = true tokio = { workspace = true, features = ["full"] } tracing.workspace = true moka.workspace = true -tokio-util = "0.7.10" +tokio-util = "0.7.11" + +[dev-dependencies] +serial_test = { workspace = true } diff --git a/crates/federate/src/lib.rs b/crates/federate/src/lib.rs index 981636345..1a5605569 100644 --- a/crates/federate/src/lib.rs +++ b/crates/federate/src/lib.rs @@ -9,17 +9,21 @@ use lemmy_api_common::{ use lemmy_db_schema::{ newtypes::InstanceId, source::{federation_queue_state::FederationQueueState, instance::Instance}, - utils::{ActualDbPool, DbPool}, }; +use lemmy_utils::error::LemmyResult; +use stats::receive_print_stats; use std::{collections::HashMap, time::Duration}; use tokio::{ - sync::mpsc::{unbounded_channel, UnboundedReceiver}, + sync::mpsc::{unbounded_channel, UnboundedSender}, + task::JoinHandle, time::sleep, }; use tokio_util::sync::CancellationToken; +use tracing::info; mod inboxes; mod send; +mod stats; mod util; mod worker; @@ -37,168 +41,326 @@ pub struct Opts { pub process_index: i32, } -async fn start_stop_federation_workers( +pub struct SendManager { opts: Opts, - pool: ActualDbPool, - federation_lib_config: FederationConfig, + workers: HashMap, + context: FederationConfig, + stats_sender: UnboundedSender<(InstanceId, FederationQueueState)>, + exit_print: JoinHandle<()>, federation_worker_config: FederationWorkerConfig, - cancel: CancellationToken, -) -> anyhow::Result<()> { - let mut workers = HashMap::::new(); +} - let (stats_sender, stats_receiver) = unbounded_channel(); - let exit_print = tokio::spawn(receive_print_stats(pool.clone(), stats_receiver)); - let pool2 = &mut DbPool::Pool(&pool); - let process_index = opts.process_index - 1; - let local_domain = federation_lib_config - .settings() - .get_hostname_without_port()?; - loop { - let mut total_count = 0; - let mut dead_count = 0; - let mut disallowed_count = 0; - for (instance, allowed, is_dead) in - Instance::read_federated_with_blocked_and_dead(pool2).await? - { - if instance.domain == local_domain { - continue; +impl SendManager { + fn new( + opts: Opts, + context: FederationConfig, + federation_worker_config: FederationWorkerConfig, + ) -> Self { + assert!(opts.process_count > 0); + assert!(opts.process_index > 0); + assert!(opts.process_index <= opts.process_count); + + let (stats_sender, stats_receiver) = unbounded_channel(); + Self { + opts, + workers: HashMap::new(), + stats_sender, + exit_print: tokio::spawn(receive_print_stats( + context.inner_pool().clone(), + stats_receiver, + )), + context, + federation_worker_config, + } + } + + pub fn run(opts: Opts, context: FederationConfig, config: FederationWorkerConfig) -> CancellableTask { + CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |cancel| { + let opts = opts.clone(); + let context = context.clone(); + let mut manager = Self::new(opts, context, config); + async move { + let result = manager.do_loop(cancel).await; + // the loop function will only return if there is (a) an internal error (e.g. db connection + // failure) or (b) it was cancelled from outside. + if let Err(e) = result { + // don't let this error bubble up, just log it, so the below cancel function will run + // regardless + tracing::error!("SendManager failed: {e}"); + } + // cancel all the dependent workers as well to ensure they don't get orphaned and keep + // running. + manager.cancel().await?; + LemmyResult::Ok(()) + // if the task was not intentionally cancelled, then this whole lambda will be run again by + // CancellableTask after this } - if instance.id.inner() % opts.process_count != process_index { - continue; - } - total_count += 1; - if !allowed { - disallowed_count += 1; - } - if is_dead { - dead_count += 1; - } - let should_federate = allowed && !is_dead; - if should_federate { - if workers.contains_key(&instance.id) { - // worker already running + }) + } + + async fn do_loop(&mut self, cancel: CancellationToken) -> LemmyResult<()> { + let process_index = self.opts.process_index - 1; + info!( + "Starting federation workers for process count {} and index {}", + self.opts.process_count, process_index + ); + let local_domain = self.context.settings().get_hostname_without_port()?; + let mut pool = self.context.pool(); + loop { + let mut total_count = 0; + let mut dead_count = 0; + let mut disallowed_count = 0; + for (instance, allowed, is_dead) in + Instance::read_federated_with_blocked_and_dead(&mut pool).await? + { + if instance.domain == local_domain { continue; } - // create new worker - let config = federation_lib_config.clone(); - let stats_sender = stats_sender.clone(); - let federation_worker_config = federation_worker_config.clone(); - workers.insert( - instance.id, - CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| { - InstanceWorker::init_and_loop( - instance.clone(), - config.clone(), - federation_worker_config.clone(), - stop, - stats_sender.clone(), - ) - }), - ); - } else if !should_federate { - if let Some(worker) = workers.remove(&instance.id) { - if let Err(e) = worker.cancel().await { - tracing::error!("error stopping worker: {e}"); + if instance.id.inner() % self.opts.process_count != process_index { + continue; + } + total_count += 1; + if !allowed { + disallowed_count += 1; + } + if is_dead { + dead_count += 1; + } + let should_federate = allowed && !is_dead; + if should_federate { + if self.workers.contains_key(&instance.id) { + // worker already running + continue; + } + // create new worker + let context = self.context.clone(); + let stats_sender = self.stats_sender.clone(); + let federation_worker_config = federation_worker_config.clone(); + + self.workers.insert( + instance.id, + CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| { + // if the instance worker ends unexpectedly due to internal/db errors, this lambda is + // rerun by cancellabletask. + let instance = instance.clone(); + let req_data = context.to_request_data(); + InstanceWorker::init_and_loop( + instance, + req_data, + federation_worker_config.clone(), + stop, + stats_sender.clone(), + ) + }), + ); + } else if !should_federate { + if let Some(worker) = self.workers.remove(&instance.id) { + if let Err(e) = worker.cancel().await { + tracing::error!("error stopping worker: {e}"); + } } } } - } - let worker_count = workers.len(); - tracing::info!("Federating to {worker_count}/{total_count} instances ({dead_count} dead, {disallowed_count} disallowed)"); - tokio::select! { - () = sleep(INSTANCES_RECHECK_DELAY) => {}, - _ = cancel.cancelled() => { break; } - } - } - drop(stats_sender); - tracing::warn!( - "Waiting for {} workers ({:.2?} max)", - workers.len(), - WORKER_EXIT_TIMEOUT - ); - // the cancel futures need to be awaited concurrently for the shutdown processes to be triggered concurrently - futures::future::join_all(workers.into_values().map(util::CancellableTask::cancel)).await; - exit_print.await?; - Ok(()) -} - -/// starts and stops federation workers depending on which instances are on db -/// await the returned future to stop/cancel all workers gracefully -pub fn start_stop_federation_workers_cancellable( - opts: Opts, - pool: ActualDbPool, - config: FederationConfig, - federation_worker_config: FederationWorkerConfig, -) -> CancellableTask { - CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| { - start_stop_federation_workers( - opts.clone(), - pool.clone(), - config.clone(), - federation_worker_config.clone(), - stop, - ) - }) -} - -/// every 60s, print the state for every instance. exits if the receiver is done (all senders dropped) -async fn receive_print_stats( - pool: ActualDbPool, - mut receiver: UnboundedReceiver<(String, FederationQueueState)>, -) { - let pool = &mut DbPool::Pool(&pool); - let mut printerval = tokio::time::interval(Duration::from_secs(60)); - printerval.tick().await; // skip first - let mut stats = HashMap::new(); - loop { - tokio::select! { - ele = receiver.recv() => { - let Some((domain, ele)) = ele else { - print_stats(pool, &stats).await; - return; - }; - stats.insert(domain, ele); - }, - _ = printerval.tick() => { - print_stats(pool, &stats).await; + let worker_count = self.workers.len(); + tracing::info!("Federating to {worker_count}/{total_count} instances ({dead_count} dead, {disallowed_count} disallowed)"); + tokio::select! { + () = sleep(INSTANCES_RECHECK_DELAY) => {}, + _ = cancel.cancelled() => { return Ok(()) } } } } + + pub async fn cancel(self) -> LemmyResult<()> { + drop(self.stats_sender); + tracing::warn!( + "Waiting for {} workers ({:.2?} max)", + self.workers.len(), + WORKER_EXIT_TIMEOUT + ); + // the cancel futures need to be awaited concurrently for the shutdown processes to be triggered + // concurrently + futures::future::join_all( + self + .workers + .into_values() + .map(util::CancellableTask::cancel), + ) + .await; + self.exit_print.await?; + Ok(()) + } } -async fn print_stats(pool: &mut DbPool<'_>, stats: &HashMap) { - let last_id = crate::util::get_latest_activity_id(pool).await; - let Ok(last_id) = last_id else { - tracing::error!("could not get last id"); - return; +#[cfg(test)] +#[allow(clippy::unwrap_used)] +#[allow(clippy::indexing_slicing)] +mod test { + + use super::*; + use activitypub_federation::config::Data; + use chrono::DateTime; + use lemmy_db_schema::source::{ + federation_allowlist::FederationAllowList, + federation_blocklist::FederationBlockList, + instance::InstanceForm, }; - // it's expected that the values are a bit out of date, everything < SAVE_STATE_EVERY should be considered up to date - tracing::info!( - "Federation state as of {}:", - Local::now() - .with_nanosecond(0) - .expect("0 is valid nanos") - .to_rfc3339() - ); - // todo: more stats (act/sec, avg http req duration) - let mut ok_count = 0; - let mut behind_count = 0; - for (domain, stat) in stats { - let behind = last_id.0 - stat.last_successful_id.map(|e| e.0).unwrap_or(0); - if stat.fail_count > 0 { - tracing::info!( - "{}: Warning. {} behind, {} consecutive fails, current retry delay {:.2?}", - domain, - behind, - stat.fail_count, - federate_retry_sleep_duration(stat.fail_count) - ); - } else if behind > 0 { - tracing::debug!("{}: Ok. {} activities behind", domain, behind); - behind_count += 1; - } else { - ok_count += 1; + use lemmy_utils::error::LemmyError; + use serial_test::serial; + use std::{ + collections::HashSet, + sync::{Arc, Mutex}, + }; + use tokio::{spawn, time::sleep}; + + struct TestData { + send_manager: SendManager, + context: Data, + instances: Vec, + } + impl TestData { + async fn init(process_count: i32, process_index: i32) -> LemmyResult { + let context = LemmyContext::init_test_context().await; + let opts = Opts { + process_count, + process_index, + }; + let federation_config = FederationConfig::builder() + .domain("local.com") + .app_data(context.clone()) + .build() + .await?; + let federation_worker_config = FederationWorkerConfig::default(); // TODO + + let pool = &mut context.pool(); + let instances = vec![ + Instance::read_or_create(pool, "alpha.com".to_string()).await?, + Instance::read_or_create(pool, "beta.com".to_string()).await?, + Instance::read_or_create(pool, "gamma.com".to_string()).await?, + ]; + + let send_manager = SendManager::new(opts, federation_config, federation_worker_config); + Ok(Self { + send_manager, + context, + instances, + }) + } + + async fn run(&mut self) -> LemmyResult<()> { + // start it and cancel after workers are running + let cancel = CancellationToken::new(); + let cancel_ = cancel.clone(); + spawn(async move { + sleep(Duration::from_millis(100)).await; + cancel_.cancel(); + }); + self.send_manager.do_loop(cancel.clone()).await?; + Ok(()) + } + + async fn cleanup(self) -> LemmyResult<()> { + self.send_manager.cancel().await?; + Instance::delete_all(&mut self.context.pool()).await?; + Ok(()) } } - tracing::info!("{ok_count} instances up to date. {behind_count} instances behind."); + + /// Basic test with default params and only active/allowed instances + #[tokio::test] + #[serial] + async fn test_send_manager() -> LemmyResult<()> { + let mut data = TestData::init(1, 1).await?; + + data.run().await?; + assert_eq!(3, data.send_manager.workers.len()); + let workers: HashSet<_> = data.send_manager.workers.keys().cloned().collect(); + let instances: HashSet<_> = data.instances.iter().map(|i| i.id).collect(); + assert_eq!(instances, workers); + + data.cleanup().await?; + Ok(()) + } + + /// Running with multiple processes should start correct workers + #[tokio::test] + #[serial] + async fn test_send_manager_processes() -> LemmyResult<()> { + let active = Arc::new(Mutex::new(vec![])); + let execute = |count, index, active: Arc>>| async move { + let mut data = TestData::init(count, index).await?; + data.run().await?; + assert_eq!(1, data.send_manager.workers.len()); + for k in data.send_manager.workers.keys() { + active.lock().unwrap().push(*k); + } + data.cleanup().await?; + Ok::<(), LemmyError>(()) + }; + execute(3, 1, active.clone()).await?; + execute(3, 2, active.clone()).await?; + execute(3, 3, active.clone()).await?; + + // Should run exactly three workers + assert_eq!(3, active.lock().unwrap().len()); + + Ok(()) + } + + /// Use blocklist, should not send to blocked instances + #[tokio::test] + #[serial] + async fn test_send_manager_blocked() -> LemmyResult<()> { + let mut data = TestData::init(1, 1).await?; + + let domain = data.instances[0].domain.clone(); + FederationBlockList::replace(&mut data.context.pool(), Some(vec![domain])).await?; + data.run().await?; + let workers = &data.send_manager.workers; + assert_eq!(2, workers.len()); + assert!(workers.contains_key(&data.instances[1].id)); + assert!(workers.contains_key(&data.instances[2].id)); + + data.cleanup().await?; + Ok(()) + } + + /// Use allowlist, should only send to allowed instance + #[tokio::test] + #[serial] + async fn test_send_manager_allowed() -> LemmyResult<()> { + let mut data = TestData::init(1, 1).await?; + + let domain = data.instances[0].domain.clone(); + FederationAllowList::replace(&mut data.context.pool(), Some(vec![domain])).await?; + data.run().await?; + let workers = &data.send_manager.workers; + assert_eq!(1, workers.len()); + assert!(workers.contains_key(&data.instances[0].id)); + + data.cleanup().await?; + Ok(()) + } + + /// Mark instance as dead, there should be no worker created for it + #[tokio::test] + #[serial] + async fn test_send_manager_dead() -> LemmyResult<()> { + let mut data = TestData::init(1, 1).await?; + + let instance = &data.instances[0]; + let form = InstanceForm::builder() + .domain(instance.domain.clone()) + .updated(DateTime::from_timestamp(0, 0)) + .build(); + Instance::update(&mut data.context.pool(), instance.id, form).await?; + + data.run().await?; + let workers = &data.send_manager.workers; + assert_eq!(2, workers.len()); + assert!(workers.contains_key(&data.instances[1].id)); + assert!(workers.contains_key(&data.instances[2].id)); + + data.cleanup().await?; + Ok(()) + } } diff --git a/crates/federate/src/stats.rs b/crates/federate/src/stats.rs new file mode 100644 index 000000000..bb6510263 --- /dev/null +++ b/crates/federate/src/stats.rs @@ -0,0 +1,97 @@ +use crate::util::get_latest_activity_id; +use chrono::Local; +use diesel::result::Error::NotFound; +use lemmy_api_common::federate_retry_sleep_duration; +use lemmy_db_schema::{ + newtypes::InstanceId, + source::{federation_queue_state::FederationQueueState, instance::Instance}, + utils::{ActualDbPool, DbPool}, +}; +use lemmy_utils::{error::LemmyResult, CACHE_DURATION_FEDERATION}; +use moka::future::Cache; +use once_cell::sync::Lazy; +use std::{collections::HashMap, time::Duration}; +use tokio::{sync::mpsc::UnboundedReceiver, time::interval}; +use tracing::{debug, info, warn}; + +/// every 60s, print the state for every instance. exits if the receiver is done (all senders +/// dropped) +pub(crate) async fn receive_print_stats( + pool: ActualDbPool, + mut receiver: UnboundedReceiver<(InstanceId, FederationQueueState)>, +) { + let pool = &mut DbPool::Pool(&pool); + let mut printerval = interval(Duration::from_secs(60)); + let mut stats = HashMap::new(); + loop { + tokio::select! { + ele = receiver.recv() => { + match ele { + // update stats for instance + Some((instance_id, ele)) => {stats.insert(instance_id, ele);}, + // receiver closed, print stats and exit + None => { + print_stats(pool, &stats).await; + return; + } + } + }, + _ = printerval.tick() => { + print_stats(pool, &stats).await; + } + } + } +} + +async fn print_stats(pool: &mut DbPool<'_>, stats: &HashMap) { + let res = print_stats_with_error(pool, stats).await; + if let Err(e) = res { + warn!("Failed to print stats: {e}"); + } +} + +async fn print_stats_with_error( + pool: &mut DbPool<'_>, + stats: &HashMap, +) -> LemmyResult<()> { + static INSTANCE_CACHE: Lazy>> = Lazy::new(|| { + Cache::builder() + .max_capacity(1) + .time_to_live(CACHE_DURATION_FEDERATION) + .build() + }); + let instances = INSTANCE_CACHE + .try_get_with((), async { Instance::read_all(pool).await }) + .await?; + + let last_id = get_latest_activity_id(pool).await?; + + // it's expected that the values are a bit out of date, everything < SAVE_STATE_EVERY should be + // considered up to date + info!("Federation state as of {}:", Local::now().to_rfc3339()); + // todo: more stats (act/sec, avg http req duration) + let mut ok_count = 0; + let mut behind_count = 0; + for (instance_id, stat) in stats { + let domain = &instances + .iter() + .find(|i| &i.id == instance_id) + .ok_or(NotFound)? + .domain; + let behind = last_id.0 - stat.last_successful_id.map(|e| e.0).unwrap_or(0); + if stat.fail_count > 0 { + info!( + "{domain}: Warning. {behind} behind, {} consecutive fails, current retry delay {:.2?}", + stat.fail_count, + federate_retry_sleep_duration(stat.fail_count) + ); + } else if behind > 0 { + debug!("{}: Ok. {} activities behind", domain, behind); + behind_count += 1; + } else { + ok_count += 1; + } + } + info!("{ok_count} others up to date. {behind_count} instances behind."); + Ok(()) +} diff --git a/crates/federate/src/util.rs b/crates/federate/src/util.rs index cedf3387f..08186f436 100644 --- a/crates/federate/src/util.rs +++ b/crates/federate/src/util.rs @@ -26,7 +26,8 @@ use tokio::{task::JoinHandle, time::sleep}; use tokio_util::sync::CancellationToken; /// Decrease the delays of the federation queue. -/// Should only be used for federation tests since it significantly increases CPU and DB load of the federation queue. +/// Should only be used for federation tests since it significantly increases CPU and DB load of the +/// federation queue. pub(crate) static LEMMY_TEST_FAST_FEDERATION: Lazy = Lazy::new(|| { std::env::var("LEMMY_TEST_FAST_FEDERATION") .map(|s| !s.is_empty()) @@ -35,9 +36,10 @@ pub(crate) static LEMMY_TEST_FAST_FEDERATION: Lazy = Lazy::new(|| { /// Recheck for new federation work every n seconds. /// -/// When the queue is processed faster than new activities are added and it reaches the current time with an empty batch, -/// this is the delay the queue waits before it checks if new activities have been added to the sent_activities table. -/// This delay is only applied if no federated activity happens during sending activities of the last batch. +/// When the queue is processed faster than new activities are added and it reaches the current time +/// with an empty batch, this is the delay the queue waits before it checks if new activities have +/// been added to the sent_activities table. This delay is only applied if no federated activity +/// happens during sending activities of the last batch. pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy = Lazy::new(|| { if *LEMMY_TEST_FAST_FEDERATION { Duration::from_millis(100) @@ -47,7 +49,8 @@ pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy = Lazy::new(|| { }); /// A task that will be run in an infinite loop, unless it is cancelled. -/// If the task exits without being cancelled, an error will be logged and the task will be restarted. +/// If the task exits without being cancelled, an error will be logged and the task will be +/// restarted. pub struct CancellableTask { f: Pin> + Send + 'static>>, } @@ -60,6 +63,7 @@ impl CancellableTask { ) -> CancellableTask where F: Future + Send + 'static, + R: Send + 'static, { let stop = CancellationToken::new(); let stop2 = stop.clone(); @@ -79,13 +83,12 @@ impl CancellableTask { stop.cancel(); tokio::select! { r = task => { - r.context("could not join")?; - Ok(()) + r.context("CancellableTask failed to cancel cleanly, returned error")?; + Ok(()) }, _ = sleep(timeout) => { abort.abort(); - tracing::warn!("Graceful shutdown timed out, aborting task"); - Err(anyhow!("task aborted due to timeout")) + Err(anyhow!("CancellableTask aborted due to shutdown timeout")) } } }), @@ -151,7 +154,6 @@ pub(crate) async fn get_activity_cached( .try_get_with(activity_id, async { let row = SentActivity::read(pool, activity_id) .await - .optional() .context("could not read activity")?; let Some(mut row) = row else { return anyhow::Result::<_, anyhow::Error>::Ok(None); diff --git a/crates/federate/src/worker.rs b/crates/federate/src/worker.rs index c282e482a..2925c047d 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -37,7 +37,7 @@ pub(crate) struct InstanceWorker { stop: CancellationToken, federation_lib_config: FederationConfig, federation_worker_config: FederationWorkerConfig, - stats_sender: UnboundedSender<(String, FederationQueueState)>, + stats_sender: UnboundedSender<(InstanceId, FederationQueueState)>, state: FederationQueueState, last_state_insert: DateTime, pool: ActualDbPool, @@ -50,7 +50,7 @@ impl InstanceWorker { config: FederationConfig, federation_worker_config: FederationWorkerConfig, stop: CancellationToken, - stats_sender: UnboundedSender<(String, FederationQueueState)>, + stats_sender: UnboundedSender<(InstanceId, FederationQueueState)>, ) -> Result<(), anyhow::Error> { let pool = config.to_request_data().inner_pool().clone(); let state = FederationQueueState::load(&mut DbPool::Pool(&pool), instance.id).await?; diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index 1403f92e7..0d18e4f1f 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -34,4 +34,4 @@ once_cell = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } urlencoding = { workspace = true } -rss = "2.0.7" +rss = "2.0.8" diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 2c29f192e..5e3db357a 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -152,7 +152,9 @@ async fn get_feed_data( limit: i64, page: i64, ) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; check_private_instance(&None, &site_view.local_site)?; @@ -257,8 +259,12 @@ async fn get_feed_user( page: &i64, user_name: &str, ) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()).await?; - let person = Person::read_from_name(&mut context.pool(), user_name, false).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let person = Person::read_from_name(&mut context.pool(), user_name, false) + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; check_private_instance(&None, &site_view.local_site)?; @@ -293,8 +299,12 @@ async fn get_feed_community( page: &i64, community_name: &str, ) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()).await?; - let community = Community::read_from_name(&mut context.pool(), community_name, false).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + let community = Community::read_from_name(&mut context.pool(), community_name, false) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; if community.visibility != CommunityVisibility::Public { return Err(LemmyErrorType::CouldntFindCommunity.into()); } @@ -336,7 +346,9 @@ async fn get_feed_front( page: &i64, jwt: &str, ) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let local_user = local_user_view_from_jwt(jwt, context).await?; check_private_instance(&Some(local_user.clone()), &site_view.local_site)?; @@ -371,7 +383,9 @@ async fn get_feed_front( #[tracing::instrument(skip_all)] async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult { - let site_view = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; let local_user = local_user_view_from_jwt(jwt, context).await?; let person_id = local_user.local_user.person_id; let show_bot_accounts = local_user.local_user.show_bot_accounts; diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 58ca9c8ef..96d7d317c 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -1,18 +1,16 @@ use actix_web::{ body::BodyStream, - error, http::{ header::{HeaderName, ACCEPT_ENCODING, HOST}, StatusCode, }, web, web::Query, - Error, HttpRequest, HttpResponse, }; use futures::stream::{Stream, StreamExt}; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{context::LemmyContext, request::PictrsResponse}; use lemmy_db_schema::source::{ images::{LocalImage, LocalImageForm, RemoteImage}, local_site::LocalSite, @@ -21,7 +19,7 @@ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{error::LemmyResult, rate_limit::RateLimitCell, REQWEST_TIMEOUT}; use reqwest::Body; use reqwest_middleware::{ClientWithMiddleware, RequestBuilder}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::time::Duration; use url::Url; use urlencoding::decode; @@ -43,20 +41,8 @@ pub fn config( .service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete))); } -#[derive(Debug, Serialize, Deserialize)] -struct Image { - file: String, - delete_token: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct Images { - msg: String, - files: Option>, -} - #[derive(Deserialize)] -struct PictrsParams { +struct PictrsGetParams { format: Option, thumbnail: Option, } @@ -66,7 +52,7 @@ fn adapt_request( client: &ClientWithMiddleware, url: String, ) -> RequestBuilder { - // remove accept-encoding header so that pictrs doesnt compress the response + // remove accept-encoding header so that pictrs doesn't compress the response const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST]; let client_request = client @@ -92,7 +78,7 @@ async fn upload( local_user_view: LocalUserView, client: web::Data, context: web::Data, -) -> Result { +) -> LemmyResult { // TODO: check rate limit here let pictrs_config = context.settings().pictrs_config()?; let image_url = format!("{}image", pictrs_config.url); @@ -106,21 +92,18 @@ async fn upload( .timeout(Duration::from_secs(pictrs_config.upload_timeout)) .body(Body::wrap_stream(make_send(body))) .send() - .await - .map_err(error::ErrorBadRequest)?; + .await?; let status = res.status(); - let images = res.json::().await.map_err(error::ErrorBadRequest)?; + let images = res.json::().await?; if let Some(images) = &images.files { - for uploaded_image in images { + for image in images { let form = LocalImageForm { local_user_id: Some(local_user_view.local_user.id), - pictrs_alias: uploaded_image.file.to_string(), - pictrs_delete_token: uploaded_image.delete_token.to_string(), + pictrs_alias: image.file.to_string(), + pictrs_delete_token: image.delete_token.to_string(), }; - LocalImage::create(&mut context.pool(), &form) - .await - .map_err(error::ErrorBadRequest)?; + LocalImage::create(&mut context.pool(), &form).await?; } } @@ -129,16 +112,14 @@ async fn upload( async fn full_res( filename: web::Path, - web::Query(params): web::Query, + web::Query(params): web::Query, req: HttpRequest, client: web::Data, context: web::Data, local_user_view: Option, -) -> Result { +) -> LemmyResult { // block access to images if instance is private and unauthorized, public - let local_site = LocalSite::read(&mut context.pool()) - .await - .map_err(error::ErrorBadRequest)?; + let local_site = LocalSite::read(&mut context.pool()).await?; if local_site.private_instance && local_user_view.is_none() { return Ok(HttpResponse::Unauthorized().finish()); } @@ -169,7 +150,7 @@ async fn image( url: String, req: HttpRequest, client: &ClientWithMiddleware, -) -> Result { +) -> LemmyResult { let mut client_req = adapt_request(&req, client, url); if let Some(addr) = req.head().peer_addr { @@ -180,7 +161,7 @@ async fn image( client_req = client_req.header("X-Forwarded-For", addr.to_string()); } - let res = client_req.send().await.map_err(error::ErrorBadRequest)?; + let res = client_req.send().await?; if res.status() == StatusCode::NOT_FOUND { return Ok(HttpResponse::NotFound().finish()); @@ -202,7 +183,7 @@ async fn delete( context: web::Data, // require login _local_user_view: LocalUserView, -) -> Result { +) -> LemmyResult { let (token, file) = components.into_inner(); let pictrs_config = context.settings().pictrs_config()?; @@ -214,11 +195,9 @@ async fn delete( client_req = client_req.header("X-Forwarded-For", addr.to_string()); } - let res = client_req.send().await.map_err(error::ErrorBadRequest)?; + let res = client_req.send().await?; - LocalImage::delete_by_alias(&mut context.pool(), &file) - .await - .map_err(error::ErrorBadRequest)?; + LocalImage::delete_by_alias(&mut context.pool(), &file).await?; Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream()))) } @@ -230,6 +209,8 @@ pub struct ImageProxyParams { pub async fn image_proxy( Query(params): Query, + req: HttpRequest, + client: web::Data, context: web::Data, ) -> LemmyResult { let url = Url::parse(&decode(¶ms.url)?)?; @@ -240,9 +221,8 @@ pub async fn image_proxy( let pictrs_config = context.settings().pictrs_config()?; let url = format!("{}image/original?proxy={}", pictrs_config.url, ¶ms.url); - let image_response = context.client().get(url).send().await?; - Ok(HttpResponse::Ok().streaming(image_response.bytes_stream())) + image(url, req, &client).await } fn make_send(mut stream: S) -> impl Stream + Send + Unpin + 'static diff --git a/crates/routes/src/lib.rs b/crates/routes/src/lib.rs index a88225622..4f8d60246 100644 --- a/crates/routes/src/lib.rs +++ b/crates/routes/src/lib.rs @@ -1,6 +1,6 @@ use lemmy_api_common::{claims::Claims, context::LemmyContext, utils::check_user_valid}; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; pub mod feeds; pub mod images; @@ -10,7 +10,9 @@ pub mod webfinger; #[tracing::instrument(skip_all)] async fn local_user_view_from_jwt(jwt: &str, context: &LemmyContext) -> LemmyResult { let local_user_id = Claims::validate(jwt, context).await?; - let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; + let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id) + .await? + .ok_or(LemmyErrorType::CouldntFindLocalUser)?; check_user_valid(&local_user_view.person)?; Ok(local_user_view) diff --git a/crates/routes/src/nodeinfo.rs b/crates/routes/src/nodeinfo.rs index b5db4e965..f64bb72c5 100644 --- a/crates/routes/src/nodeinfo.rs +++ b/crates/routes/src/nodeinfo.rs @@ -14,10 +14,12 @@ use url::Url; pub fn config(cfg: &mut web::ServiceConfig) { cfg .route( - "/nodeinfo/2.0.json", + "/nodeinfo/2.1.json", web::get().to(node_info).wrap(cache_1hour()), ) - .service(web::redirect("/version", "/nodeinfo/2.0.json")) + .service(web::redirect("/version", "/nodeinfo/2.1.json")) + // For backwards compatibility, can be removed after Lemmy 0.20 + .service(web::redirect("/nodeinfo/2.0.json", "/nodeinfo/2.1.json")) .route( "/.well-known/nodeinfo", web::get().to(node_info_well_known).wrap(cache_3days()), @@ -27,9 +29,9 @@ pub fn config(cfg: &mut web::ServiceConfig) { async fn node_info_well_known(context: web::Data) -> LemmyResult { let node_info = NodeInfoWellKnown { links: vec![NodeInfoWellKnownLinks { - rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0")?, + rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.1")?, href: Url::parse(&format!( - "{}/nodeinfo/2.0.json", + "{}/nodeinfo/2.1.json", &context.settings().get_protocol_and_hostname(), ))?, }], @@ -40,23 +42,21 @@ async fn node_info_well_known(context: web::Data) -> LemmyResult) -> Result { let site_view = SiteView::read_local(&mut context.pool()) .await - .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?; + .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))? + .ok_or(ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?; - let protocols = if site_view.local_site.federation_enabled { - Some(vec!["activitypub".to_string()]) - } else { - None - }; // Since there are 3 registration options, // we need to set open_registrations as true if RegistrationMode is not Closed. let open_registrations = Some(site_view.local_site.registration_mode != RegistrationMode::Closed); let json = NodeInfo { - version: Some("2.0".to_string()), + version: Some("2.1".to_string()), software: Some(NodeInfoSoftware { name: Some("lemmy".to_string()), version: Some(VERSION.to_string()), + repository: Some("https://github.com/LemmyNet/lemmy".to_string()), + homepage: Some("https://join-lemmy.org/".to_string()), }), - protocols, + protocols: Some(vec!["activitypub".to_string()]), usage: Some(NodeInfoUsage { users: Some(NodeInfoUsers { total: Some(site_view.counts.users), @@ -67,6 +67,11 @@ async fn node_info(context: web::Data) -> Result>, pub usage: Option, pub open_registrations: Option, + /// These fields are required by the spec for no reason + pub services: Option, + pub metadata: Option>, } #[derive(Serialize, Deserialize, Debug, Default)] @@ -98,6 +107,8 @@ pub struct NodeInfo { pub struct NodeInfoSoftware { pub name: Option, pub version: Option, + pub repository: Option, + pub homepage: Option, } #[derive(Serialize, Deserialize, Debug, Default)] @@ -115,3 +126,10 @@ pub struct NodeInfoUsers { pub active_halfyear: Option, pub active_month: Option, } + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase", default)] +pub struct NodeInfoServices { + pub inbound: Option>, + pub outbound: Option>, +} diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs index 36dd97079..f2a67c0fc 100644 --- a/crates/routes/src/webfinger.rs +++ b/crates/routes/src/webfinger.rs @@ -47,10 +47,12 @@ async fn get_webfinger_response( let user_id: Option = Person::read_from_name(&mut context.pool(), name, false) .await .ok() + .flatten() .map(|c| c.actor_id.into()); let community_id: Option = Community::read_from_name(&mut context.pool(), name, false) .await .ok() + .flatten() .and_then(|c| { if c.visibility == CommunityVisibility::Public { let id: Url = c.actor_id.into(); @@ -61,7 +63,7 @@ async fn get_webfinger_response( }); // Mastodon seems to prioritize the last webfinger item in case of duplicates. Put - // community last so that it gets prioritized. For Lemmy the order doesnt matter. + // community last so that it gets prioritized. For Lemmy the order doesn't matter. vec![ webfinger_link_for_actor(user_id, "Person", &context), webfinger_link_for_actor(community_id, "Group", &context), diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 0a1b22c32..046c0a872 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -75,10 +75,10 @@ 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.6.0", 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.4", features = [ +lettre = { version = "0.11.7", features = [ "tokio1", "tokio1-native-tls", ], optional = true } diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index cc6897edd..324c08ccb 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -43,6 +43,16 @@ pub enum LemmyErrorType { BannedFromCommunity, CouldntFindCommunity, CouldntFindPerson, + CouldntFindComment, + CouldntFindCommentReport, + CouldntFindPostReport, + CouldntFindPrivateMessageReport, + CouldntFindLocalUser, + CouldntFindPersonMention, + CouldntFindRegistrationApplication, + CouldntFindCommentReply, + CouldntFindPrivateMessage, + CouldntFindActivity, PersonIsBlocked, CommunityIsBlocked, InstanceIsBlocked, @@ -89,9 +99,8 @@ pub enum LemmyErrorType { PersonIsBannedFromSite(String), InvalidVoteValue, PageDoesNotSpecifyCreator, - PageDoesNotSpecifyGroup, - NoCommunityFoundInCc, NoEmailSetup, + LocalSiteNotSetup, EmailSmtpServerNeedsAPort, MissingAnEmail, RateLimitError, @@ -152,19 +161,21 @@ pub enum LemmyErrorType { PermissiveRegex, InvalidRegex, CaptchaIncorrect, - PasswordResetLimitReached, CouldntCreateAudioCaptcha, InvalidUrlScheme, CouldntSendWebmention, ContradictingFilters, InstanceBlockAlreadyExists, - /// Thrown when an API call is submitted with more than 1000 array elements, see [[MAX_API_PARAM_ELEMENTS]] + /// Thrown when an API call is submitted with more than 1000 array elements, see + /// [[MAX_API_PARAM_ELEMENTS]] TooManyItems, CommunityHasNoFollowers, BanExpirationInPast, InvalidUnixTime, InvalidBotAction, CantBlockLocalInstance, + UrlWithoutDomain, + InboxTimeout, Unknown(String), } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 95c1d0144..1adb3f6cf 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -56,6 +56,7 @@ pub fn spawn_try_task( tracing::warn!("error in spawn: {e}"); } } - .in_current_span(), // this makes sure the inner tracing gets the same context as where spawn was called + .in_current_span(), /* this makes sure the inner tracing gets the same context as where + * spawn was called */ ); } diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index 3f89aee7e..5f1b6f7f5 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -46,9 +46,10 @@ impl Bucket { fn update(self, now: InstantSecs, config: BucketConfig) -> Self { let secs_since_last_checked = now.secs.saturating_sub(self.last_checked.secs); - // For `secs_since_last_checked` seconds, the amount of tokens increases by `capacity` every `secs_to_refill` seconds. - // The amount of tokens added per second is `capacity / secs_to_refill`. - // The expression below is like `secs_since_last_checked * (capacity / secs_to_refill)` but with precision and non-overflowing multiplication. + // For `secs_since_last_checked` seconds, the amount of tokens increases by `capacity` every + // `secs_to_refill` seconds. The amount of tokens added per second is `capacity / + // secs_to_refill`. The expression below is like `secs_since_last_checked * (capacity / + // secs_to_refill)` but with precision and non-overflowing multiplication. let added_tokens = u64::from(secs_since_last_checked) * u64::from(config.capacity) / u64::from(config.secs_to_refill); @@ -124,8 +125,9 @@ impl MapLevel for Map { ..config }); - // Remove groups that are no longer needed if the hash map's existing allocation has no space for new groups. - // This is done before calling `HashMap::entry` because that immediately allocates just like `HashMap::insert`. + // Remove groups that are no longer needed if the hash map's existing allocation has no space + // for new groups. This is done before calling `HashMap::entry` because that immediately + // allocates just like `HashMap::insert`. if (self.capacity() == self.len()) && !self.contains_key(&addr_part) { self.remove_full_buckets(now, configs); } @@ -219,7 +221,8 @@ impl RateLimitedGroup { if new_bucket.tokens == 0 { // Not enough tokens yet - // Setting `bucket` to `new_bucket` here is useless and would cause the bucket to start over at 0 tokens because of rounding + // Setting `bucket` to `new_bucket` here is useless and would cause the bucket to start over + // at 0 tokens because of rounding false } else { // Consume 1 token @@ -239,10 +242,12 @@ pub struct RateLimitState { /// /// The same thing happens for the first 48 and 56 bits, but with increased capacity. /// - /// This is done because all users can easily switch to any other IPv6 address that has the same first 64 bits. - /// It could be as low as 48 bits for some networks, which is the reason for 48 and 56 bit address groups. + /// This is done because all users can easily switch to any other IPv6 address that has the same + /// first 64 bits. It could be as low as 48 bits for some networks, which is the reason for 48 + /// and 56 bit address groups. ipv6_buckets: Map<[u8; 6], Map>>, - /// This stores a `BucketConfig` for each `ActionType`. `EnumMap` makes it impossible to have a missing `BucketConfig`. + /// This stores a `BucketConfig` for each `ActionType`. `EnumMap` makes it impossible to have a + /// missing `BucketConfig`. bucket_configs: EnumMap, } @@ -339,7 +344,8 @@ mod tests { let mut rate_limiter = RateLimitState::new(bucket_configs); let mut now = InstantSecs::now(); - // Do 1 `Message` and 1 `Post` action for each IP address, and expect the limit to not be reached + // Do 1 `Message` and 1 `Post` action for each IP address, and expect the limit to not be + // reached let ips = [ "123.123.123.123", "1:2:3::", diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index c5394a908..8a2c7dd27 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -38,7 +38,8 @@ pub struct Settings { /// Whether the site is available over TLS. Needs to be true for federation to work. #[default(true)] pub tls_enabled: bool, - /// Set the URL for opentelemetry exports. If you do not have an opentelemetry collector, do not set this option + /// Set the URL for opentelemetry exports. If you do not have an opentelemetry collector, do not + /// set this option #[default(None)] #[doku(skip)] pub opentelemetry_url: Option, @@ -81,7 +82,8 @@ pub struct PictrsConfig { /// To be removed in 0.20 pub(super) cache_external_link_previews: Option, - /// Specifies how to handle remote images, so that users don't have to connect directly to remote servers. + /// Specifies how to handle remote images, so that users don't have to connect directly to remote + /// servers. #[default(PictrsImageMode::StoreLinkPreviews)] pub(super) image_mode: PictrsImageMode, @@ -93,7 +95,7 @@ pub struct PictrsConfig { #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document, PartialEq)] #[serde(deny_unknown_fields)] pub enum PictrsImageMode { - /// Leave images unchanged, don't generate any local thumbnails for post urls. Instead the the + /// Leave images unchanged, don't generate any local thumbnails for post urls. Instead the /// Opengraph image is directly returned as thumbnail None, /// Generate thumbnails for external post urls and store them persistently in pict-rs. This @@ -103,10 +105,11 @@ pub enum PictrsImageMode { /// This is the default behaviour, and also matches Lemmy 0.18. #[default] StoreLinkPreviews, - /// If enabled, all images from remote domains are rewritten to pass through `/api/v3/image_proxy`, - /// including embedded images in markdown. Images are stored temporarily in pict-rs for caching. - /// This improves privacy as users don't expose their IP to untrusted servers, and decreases load - /// on other servers. However it increases bandwidth use for the local server. + /// If enabled, all images from remote domains are rewritten to pass through + /// `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily + /// in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted + /// servers, and decreases load on other servers. However it increases bandwidth use for the + /// local server. /// /// Requires pict-rs 0.5 ProxyAllImages, diff --git a/crates/utils/src/utils/slurs.rs b/crates/utils/src/utils/slurs.rs index c7c8a88e3..ba94372fa 100644 --- a/crates/utils/src/utils/slurs.rs +++ b/crates/utils/src/utils/slurs.rs @@ -101,7 +101,7 @@ mod test { // These helped with testing // #[test] // fn test_send_email() { - // let result = send_email("not a subject", "test_email@gmail.com", "ur user", "

HI there

"); - // assert!(result.is_ok()); + // let result = send_email("not a subject", "test_email@gmail.com", "ur user", "

HI + // there

"); assert!(result.is_ok()); // } } diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index 93d581327..a913e6243 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -19,7 +19,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 ALT_TEXT_MAX_LENGTH: usize = 300; +const ALT_TEXT_MAX_LENGTH: usize = 1500; const SITE_NAME_MAX_LENGTH: usize = 20; const SITE_NAME_MIN_LENGTH: usize = 1; const SITE_DESCRIPTION_MAX_LENGTH: usize = 150; @@ -309,21 +309,44 @@ pub fn is_url_blocked(url: &Option, blocklist: &RegexSet) -> LemmyResult<() Ok(()) } +/// Check that urls are valid, and also remove the scheme, and uniques pub fn check_urls_are_valid(urls: &Vec) -> LemmyResult> { let mut parsed_urls = vec![]; for url in urls { - let url = Url::parse(url).or_else(|e| { - if e == ParseError::RelativeUrlWithoutBase { - Url::parse(&format!("https://{url}")) - } else { - Err(e) - } - })?; - - parsed_urls.push(url.to_string()); + parsed_urls.push(build_url_str_without_scheme(url)?); } - Ok(parsed_urls) + let unique_urls = parsed_urls.into_iter().unique().collect(); + Ok(unique_urls) +} + +pub fn build_url_str_without_scheme(url_str: &str) -> LemmyResult { + // Parse and check for errors + let mut url = Url::parse(url_str).or_else(|e| { + if e == ParseError::RelativeUrlWithoutBase { + Url::parse(&format!("http://{url_str}")) + } else { + Err(e) + } + })?; + + // Set the scheme to http, then remove the http:// part + url + .set_scheme("http") + .map_err(|_| LemmyErrorType::InvalidUrl)?; + + let mut out = url + .to_string() + .get(7..) + .ok_or(LemmyErrorType::InvalidUrl)? + .to_string(); + + // Remove trailing / if necessary + if out.ends_with('/') { + out.pop(); + } + + Ok(out) } #[cfg(test)] @@ -600,17 +623,21 @@ mod tests { #[test] fn test_url_parsed() { + // Make sure the scheme is removed, and uniques also assert_eq!( - vec![String::from("https://example.com/")], - check_urls_are_valid(&vec![String::from("example.com")]).unwrap() + &check_urls_are_valid(&vec![ + "example.com".to_string(), + "http://example.com".to_string(), + "https://example.com".to_string(), + "https://example.com/test?q=test2&q2=test3#test4".to_string(), + ]) + .unwrap(), + &vec![ + "example.com".to_string(), + "example.com/test?q=test2&q2=test3#test4".to_string() + ], ); - assert!(check_urls_are_valid(&vec![ - String::from("example.com"), - String::from("https://example.blog") - ]) - .is_ok()); - - assert!(check_urls_are_valid(&vec![String::from("https://example .com"),]).is_err()); + assert!(check_urls_are_valid(&vec!["https://example .com".to_string()]).is_err()); } } diff --git a/crates/utils/translations b/crates/utils/translations index 3d268fbca..e9b3b25fa 160000 --- a/crates/utils/translations +++ b/crates/utils/translations @@ -1 +1 @@ -Subproject commit 3d268fbca717648b1153094ba7eac931791a4fef +Subproject commit e9b3b25fa1af7e06c4ffab86624d95da0836ef36 diff --git a/docker/Dockerfile b/docker/Dockerfile index ff3dfc7ff..25761d3c1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,11 +1,11 @@ -# syntax=docker/dockerfile:1.6 +# syntax=docker/dockerfile:1.7 ARG RUST_VERSION=1.77 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.2.0" +ARG ARM_BUILDER_IMAGE="ghcr.io/raskyld/aarch64-lemmy-linux-gnu:v0.3.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 1bf38863f..c690a5f48 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -75,7 +75,7 @@ services: init: true pictrs: - image: asonix/pictrs:0.5.0-rc.2 + image: asonix/pictrs:0.5.13 # this needs to match the pictrs url in lemmy.hjson hostname: pictrs # we can set options to pictrs like this, here we set max. image size and forced format for conversion @@ -114,10 +114,12 @@ services: "-c", "auto_explain.log_analyze=true", "-c", + "auto_explain.log_triggers=true", + "-c", "track_activity_query_size=1048576", ] ports: - # use a different port so it doesnt conflict with potential postgres db running on the host + # use a different port so it doesn't conflict with potential postgres db running on the host - "5433:5432" environment: - POSTGRES_USER=lemmy diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 8dd70f3bc..ac4ef4649 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -49,7 +49,7 @@ services: pictrs: restart: always - image: asonix/pictrs:0.5.0-rc.2 + image: asonix/pictrs:0.5.13 user: 991:991 volumes: - ./volumes/pictrs_alpha:/mnt:Z diff --git a/migrations/2024-02-24-034523_replaceable-schema/down.sql b/migrations/2024-02-24-034523_replaceable-schema/down.sql new file mode 100644 index 000000000..db8feacdf --- /dev/null +++ b/migrations/2024-02-24-034523_replaceable-schema/down.sql @@ -0,0 +1,1062 @@ +DROP SCHEMA IF EXISTS r CASCADE; + +DROP INDEX idx_site_aggregates_1_row_only; + +CREATE FUNCTION comment_aggregates_comment () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + INSERT INTO comment_aggregates (comment_id, published) + VALUES (NEW.id, NEW.published); + ELSIF (TG_OP = 'DELETE') THEN + DELETE FROM comment_aggregates + WHERE comment_id = OLD.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION comment_aggregates_score () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + UPDATE + comment_aggregates ca + SET + score = score + NEW.score, + upvotes = CASE WHEN NEW.score = 1 THEN + upvotes + 1 + ELSE + upvotes + END, + downvotes = CASE WHEN NEW.score = - 1 THEN + downvotes + 1 + ELSE + downvotes + END, + controversy_rank = controversy_rank (ca.upvotes + CASE WHEN NEW.score = 1 THEN + 1 + ELSE + 0 + END::numeric, ca.downvotes + CASE WHEN NEW.score = - 1 THEN + 1 + ELSE + 0 + END::numeric) + WHERE + ca.comment_id = NEW.comment_id; + ELSIF (TG_OP = 'DELETE') THEN + -- Join to comment because that comment may not exist anymore + UPDATE + comment_aggregates ca + SET + score = score - OLD.score, + upvotes = CASE WHEN OLD.score = 1 THEN + upvotes - 1 + ELSE + upvotes + END, + downvotes = CASE WHEN OLD.score = - 1 THEN + downvotes - 1 + ELSE + downvotes + END, + controversy_rank = controversy_rank (ca.upvotes + CASE WHEN NEW.score = 1 THEN + 1 + ELSE + 0 + END::numeric, ca.downvotes + CASE WHEN NEW.score = - 1 THEN + 1 + ELSE + 0 + END::numeric) + FROM + comment c + WHERE + ca.comment_id = c.id + AND ca.comment_id = OLD.comment_id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION community_aggregates_comment_count () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_restored_or_created (TG_OP, OLD, NEW)) THEN + UPDATE + community_aggregates ca + SET + comments = comments + 1 + FROM + post p + WHERE + p.id = NEW.post_id + AND ca.community_id = p.community_id; + ELSIF (was_removed_or_deleted (TG_OP, OLD, NEW)) THEN + UPDATE + community_aggregates ca + SET + comments = comments - 1 + FROM + post p + WHERE + p.id = OLD.post_id + AND ca.community_id = p.community_id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION community_aggregates_community () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + INSERT INTO community_aggregates (community_id, published) + VALUES (NEW.id, NEW.published); + ELSIF (TG_OP = 'DELETE') THEN + DELETE FROM community_aggregates + WHERE community_id = OLD.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION community_aggregates_post_count () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_restored_or_created (TG_OP, OLD, NEW)) THEN + UPDATE + community_aggregates + SET + posts = posts + 1 + WHERE + community_id = NEW.community_id; + IF (TG_OP = 'UPDATE') THEN + -- Post was restored, so restore comment counts as well + UPDATE + community_aggregates ca + SET + posts = coalesce(cd.posts, 0), + comments = coalesce(cd.comments, 0) + FROM ( + SELECT + c.id, + count(DISTINCT p.id) AS posts, + count(DISTINCT ct.id) AS comments + FROM + community c + LEFT JOIN post p ON c.id = p.community_id + AND p.deleted = 'f' + AND p.removed = 'f' + LEFT JOIN comment ct ON p.id = ct.post_id + AND ct.deleted = 'f' + AND ct.removed = 'f' + WHERE + c.id = NEW.community_id + GROUP BY + c.id) cd + WHERE + ca.community_id = NEW.community_id; + END IF; + ELSIF (was_removed_or_deleted (TG_OP, OLD, NEW)) THEN + UPDATE + community_aggregates + SET + posts = posts - 1 + WHERE + community_id = OLD.community_id; + -- Update the counts if the post got deleted + UPDATE + community_aggregates ca + SET + posts = coalesce(cd.posts, 0), + comments = coalesce(cd.comments, 0) + FROM ( + SELECT + c.id, + count(DISTINCT p.id) AS posts, + count(DISTINCT ct.id) AS comments + FROM + community c + LEFT JOIN post p ON c.id = p.community_id + AND p.deleted = 'f' + AND p.removed = 'f' + LEFT JOIN comment ct ON p.id = ct.post_id + AND ct.deleted = 'f' + AND ct.removed = 'f' + WHERE + c.id = OLD.community_id + GROUP BY + c.id) cd + WHERE + ca.community_id = OLD.community_id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION community_aggregates_post_count_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + community_aggregates + SET + posts = posts + post_group.count + FROM ( + SELECT + community_id, + count(*) + FROM + new_post + GROUP BY + community_id) post_group +WHERE + community_aggregates.community_id = post_group.community_id; + RETURN NULL; +END +$$; + +CREATE FUNCTION community_aggregates_subscriber_count () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + UPDATE + community_aggregates ca + SET + subscribers = subscribers + community.local::int, + subscribers_local = subscribers_local + person.local::int + FROM + community + LEFT JOIN person ON person.id = NEW.person_id + WHERE + community.id = NEW.community_id + AND community.id = ca.community_id + AND person.local IS NOT NULL; + ELSIF (TG_OP = 'DELETE') THEN + UPDATE + community_aggregates ca + SET + subscribers = subscribers - community.local::int, + subscribers_local = subscribers_local - person.local::int + FROM + community + LEFT JOIN person ON person.id = OLD.person_id + WHERE + community.id = OLD.community_id + AND community.id = ca.community_id + AND person.local IS NOT NULL; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION delete_follow_before_person () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + DELETE FROM community_follower AS c + WHERE c.person_id = OLD.id; + RETURN OLD; +END; +$$; + +CREATE FUNCTION person_aggregates_comment_count () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_restored_or_created (TG_OP, OLD, NEW)) THEN + UPDATE + person_aggregates + SET + comment_count = comment_count + 1 + WHERE + person_id = NEW.creator_id; + ELSIF (was_removed_or_deleted (TG_OP, OLD, NEW)) THEN + UPDATE + person_aggregates + SET + comment_count = comment_count - 1 + WHERE + person_id = OLD.creator_id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION person_aggregates_comment_score () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + -- Need to get the post creator, not the voter + UPDATE + person_aggregates ua + SET + comment_score = comment_score + NEW.score + FROM + comment c + WHERE + ua.person_id = c.creator_id + AND c.id = NEW.comment_id; + ELSIF (TG_OP = 'DELETE') THEN + UPDATE + person_aggregates ua + SET + comment_score = comment_score - OLD.score + FROM + comment c + WHERE + ua.person_id = c.creator_id + AND c.id = OLD.comment_id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION person_aggregates_person () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + INSERT INTO person_aggregates (person_id) + VALUES (NEW.id); + ELSIF (TG_OP = 'DELETE') THEN + DELETE FROM person_aggregates + WHERE person_id = OLD.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION person_aggregates_post_count () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_restored_or_created (TG_OP, OLD, NEW)) THEN + UPDATE + person_aggregates + SET + post_count = post_count + 1 + WHERE + person_id = NEW.creator_id; + ELSIF (was_removed_or_deleted (TG_OP, OLD, NEW)) THEN + UPDATE + person_aggregates + SET + post_count = post_count - 1 + WHERE + person_id = OLD.creator_id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION person_aggregates_post_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + person_aggregates + SET + post_count = post_count + post_group.count + FROM ( + SELECT + creator_id, + count(*) + FROM + new_post + GROUP BY + creator_id) post_group +WHERE + person_aggregates.person_id = post_group.creator_id; + RETURN NULL; +END +$$; + +CREATE FUNCTION person_aggregates_post_score () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + -- Need to get the post creator, not the voter + UPDATE + person_aggregates ua + SET + post_score = post_score + NEW.score + FROM + post p + WHERE + ua.person_id = p.creator_id + AND p.id = NEW.post_id; + ELSIF (TG_OP = 'DELETE') THEN + UPDATE + person_aggregates ua + SET + post_score = post_score - OLD.score + FROM + post p + WHERE + ua.person_id = p.creator_id + AND p.id = OLD.post_id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION post_aggregates_comment_count () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + -- Check for post existence - it may not exist anymore + IF TG_OP = 'INSERT' OR EXISTS ( + SELECT + 1 + FROM + post p + WHERE + p.id = OLD.post_id) THEN + IF (was_restored_or_created (TG_OP, OLD, NEW)) THEN + UPDATE + post_aggregates pa + SET + comments = comments + 1 + WHERE + pa.post_id = NEW.post_id; + ELSIF (was_removed_or_deleted (TG_OP, OLD, NEW)) THEN + UPDATE + post_aggregates pa + SET + comments = comments - 1 + WHERE + pa.post_id = OLD.post_id; + END IF; + END IF; + IF TG_OP = 'INSERT' THEN + UPDATE + post_aggregates pa + SET + newest_comment_time = NEW.published + WHERE + pa.post_id = NEW.post_id; + -- A 2 day necro-bump limit + UPDATE + post_aggregates pa + SET + newest_comment_time_necro = NEW.published + FROM + post p + WHERE + pa.post_id = p.id + AND pa.post_id = NEW.post_id + -- Fix issue with being able to necro-bump your own post + AND NEW.creator_id != p.creator_id + AND pa.published > ('now'::timestamp - '2 days'::interval); + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION post_aggregates_featured_community () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + post_aggregates pa + SET + featured_community = NEW.featured_community + WHERE + pa.post_id = NEW.id; + RETURN NULL; +END +$$; + +CREATE FUNCTION post_aggregates_featured_local () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + post_aggregates pa + SET + featured_local = NEW.featured_local + WHERE + pa.post_id = NEW.id; + RETURN NULL; +END +$$; + +CREATE FUNCTION post_aggregates_post () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO post_aggregates (post_id, published, newest_comment_time, newest_comment_time_necro, community_id, creator_id, instance_id) + SELECT + id, + published, + published, + published, + community_id, + creator_id, + ( + SELECT + community.instance_id + FROM + community + WHERE + community.id = community_id + LIMIT 1) +FROM + new_post; + RETURN NULL; +END +$$; + +CREATE FUNCTION post_aggregates_score () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + UPDATE + post_aggregates pa + SET + score = score + NEW.score, + upvotes = CASE WHEN NEW.score = 1 THEN + upvotes + 1 + ELSE + upvotes + END, + downvotes = CASE WHEN NEW.score = - 1 THEN + downvotes + 1 + ELSE + downvotes + END, + controversy_rank = controversy_rank (pa.upvotes + CASE WHEN NEW.score = 1 THEN + 1 + ELSE + 0 + END::numeric, pa.downvotes + CASE WHEN NEW.score = - 1 THEN + 1 + ELSE + 0 + END::numeric) + WHERE + pa.post_id = NEW.post_id; + ELSIF (TG_OP = 'DELETE') THEN + -- Join to post because that post may not exist anymore + UPDATE + post_aggregates pa + SET + score = score - OLD.score, + upvotes = CASE WHEN OLD.score = 1 THEN + upvotes - 1 + ELSE + upvotes + END, + downvotes = CASE WHEN OLD.score = - 1 THEN + downvotes - 1 + ELSE + downvotes + END, + controversy_rank = controversy_rank (pa.upvotes + CASE WHEN NEW.score = 1 THEN + 1 + ELSE + 0 + END::numeric, pa.downvotes + CASE WHEN NEW.score = - 1 THEN + 1 + ELSE + 0 + END::numeric) + FROM + post p + WHERE + pa.post_id = p.id + AND pa.post_id = OLD.post_id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_comment_delete () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_removed_or_deleted (TG_OP, OLD, NEW)) THEN + UPDATE + site_aggregates sa + SET + comments = comments - 1 + FROM + site s + WHERE + sa.site_id = s.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_comment_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_restored_or_created (TG_OP, OLD, NEW)) THEN + UPDATE + site_aggregates sa + SET + comments = comments + 1 + FROM + site s + WHERE + sa.site_id = s.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_community_delete () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_removed_or_deleted (TG_OP, OLD, NEW)) THEN + UPDATE + site_aggregates sa + SET + communities = communities - 1 + FROM + site s + WHERE + sa.site_id = s.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_community_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_restored_or_created (TG_OP, OLD, NEW)) THEN + UPDATE + site_aggregates sa + SET + communities = communities + 1 + FROM + site s + WHERE + sa.site_id = s.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_person_delete () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + -- Join to site since the creator might not be there anymore + UPDATE + site_aggregates sa + SET + users = users - 1 + FROM + site s + WHERE + sa.site_id = s.id; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_person_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + site_aggregates + SET + users = users + 1; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_post_delete () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_removed_or_deleted (TG_OP, OLD, NEW)) THEN + UPDATE + site_aggregates sa + SET + posts = posts - 1 + FROM + site s + WHERE + sa.site_id = s.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_post_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + site_aggregates sa + SET + posts = posts + ( + SELECT + count(*) + FROM + new_post) + FROM + site s + WHERE + sa.site_id = s.id; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_post_update () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (was_restored_or_created (TG_OP, OLD, NEW)) THEN + UPDATE + site_aggregates sa + SET + posts = posts + 1 + FROM + site s + WHERE + sa.site_id = s.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION site_aggregates_site () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + -- we only ever want to have a single value in site_aggregate because the site_aggregate triggers update all rows in that table. + -- a cleaner check would be to insert it for the local_site but that would break assumptions at least in the tests + IF (TG_OP = 'INSERT') AND NOT EXISTS ( + SELECT + * + FROM + site_aggregates + LIMIT 1) THEN + INSERT INTO site_aggregates (site_id) + VALUES (NEW.id); + ELSIF (TG_OP = 'DELETE') THEN + DELETE FROM site_aggregates + WHERE site_id = OLD.id; + END IF; + RETURN NULL; +END +$$; + +CREATE FUNCTION was_removed_or_deleted (tg_op text, old record, new record) + RETURNS boolean + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + RETURN FALSE; + END IF; + IF (TG_OP = 'DELETE' AND OLD.deleted = 'f' AND OLD.removed = 'f') THEN + RETURN TRUE; + END IF; + RETURN TG_OP = 'UPDATE' + AND OLD.deleted = 'f' + AND OLD.removed = 'f' + AND (NEW.deleted = 't' + OR NEW.removed = 't'); +END +$$; + +CREATE FUNCTION was_restored_or_created (tg_op text, old record, new record) + RETURNS boolean + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'DELETE') THEN + RETURN FALSE; + END IF; + IF (TG_OP = 'INSERT') THEN + RETURN TRUE; + END IF; + RETURN TG_OP = 'UPDATE' + AND NEW.deleted = 'f' + AND NEW.removed = 'f' + AND (OLD.deleted = 't' + OR OLD.removed = 't'); +END +$$; + +CREATE TRIGGER comment_aggregates_comment + AFTER INSERT OR DELETE ON comment + FOR EACH ROW + EXECUTE FUNCTION comment_aggregates_comment (); + +CREATE TRIGGER comment_aggregates_score + AFTER INSERT OR DELETE ON comment_like + FOR EACH ROW + EXECUTE FUNCTION comment_aggregates_score (); + +CREATE TRIGGER community_aggregates_comment_count + AFTER INSERT OR DELETE OR UPDATE OF removed, + deleted ON comment + FOR EACH ROW + EXECUTE FUNCTION community_aggregates_comment_count (); + +CREATE TRIGGER community_aggregates_community + AFTER INSERT OR DELETE ON community + FOR EACH ROW + EXECUTE FUNCTION community_aggregates_community (); + +CREATE TRIGGER community_aggregates_post_count + AFTER DELETE OR UPDATE OF removed, + deleted ON post + FOR EACH ROW + EXECUTE FUNCTION community_aggregates_post_count (); + +CREATE TRIGGER community_aggregates_post_count_insert + AFTER INSERT ON post REFERENCING NEW TABLE AS new_post + FOR EACH STATEMENT + EXECUTE FUNCTION community_aggregates_post_count_insert (); + +CREATE TRIGGER community_aggregates_subscriber_count + AFTER INSERT OR DELETE ON community_follower + FOR EACH ROW + EXECUTE FUNCTION community_aggregates_subscriber_count (); + +CREATE TRIGGER delete_follow_before_person + BEFORE DELETE ON person + FOR EACH ROW + EXECUTE FUNCTION delete_follow_before_person (); + +CREATE TRIGGER person_aggregates_comment_count + AFTER INSERT OR DELETE OR UPDATE OF removed, + deleted ON comment + FOR EACH ROW + EXECUTE FUNCTION person_aggregates_comment_count (); + +CREATE TRIGGER person_aggregates_comment_score + AFTER INSERT OR DELETE ON comment_like + FOR EACH ROW + EXECUTE FUNCTION person_aggregates_comment_score (); + +CREATE TRIGGER person_aggregates_person + AFTER INSERT OR DELETE ON person + FOR EACH ROW + EXECUTE FUNCTION person_aggregates_person (); + +CREATE TRIGGER person_aggregates_post_count + AFTER DELETE OR UPDATE OF removed, + deleted ON post + FOR EACH ROW + EXECUTE FUNCTION person_aggregates_post_count (); + +CREATE TRIGGER person_aggregates_post_insert + AFTER INSERT ON post REFERENCING NEW TABLE AS new_post + FOR EACH STATEMENT + EXECUTE FUNCTION person_aggregates_post_insert (); + +CREATE TRIGGER person_aggregates_post_score + AFTER INSERT OR DELETE ON post_like + FOR EACH ROW + EXECUTE FUNCTION person_aggregates_post_score (); + +CREATE TRIGGER post_aggregates_comment_count + AFTER INSERT OR DELETE OR UPDATE OF removed, + deleted ON comment + FOR EACH ROW + EXECUTE FUNCTION post_aggregates_comment_count (); + +CREATE TRIGGER post_aggregates_featured_community + AFTER UPDATE ON post + FOR EACH ROW + WHEN ((old.featured_community IS DISTINCT FROM new.featured_community)) + EXECUTE FUNCTION post_aggregates_featured_community (); + +CREATE TRIGGER post_aggregates_featured_local + AFTER UPDATE ON post + FOR EACH ROW + WHEN ((old.featured_local IS DISTINCT FROM new.featured_local)) + EXECUTE FUNCTION post_aggregates_featured_local (); + +CREATE TRIGGER post_aggregates_post + AFTER INSERT ON post REFERENCING NEW TABLE AS new_post + FOR EACH STATEMENT + EXECUTE FUNCTION post_aggregates_post (); + +CREATE TRIGGER post_aggregates_score + AFTER INSERT OR DELETE ON post_like + FOR EACH ROW + EXECUTE FUNCTION post_aggregates_score (); + +CREATE TRIGGER site_aggregates_comment_delete + AFTER DELETE OR UPDATE OF removed, + deleted ON comment + FOR EACH ROW + WHEN ((old.local = TRUE)) + EXECUTE FUNCTION site_aggregates_comment_delete (); + +CREATE TRIGGER site_aggregates_comment_insert + AFTER INSERT OR UPDATE OF removed, + deleted ON comment + FOR EACH ROW + WHEN ((new.local = TRUE)) + EXECUTE FUNCTION site_aggregates_comment_insert (); + +CREATE TRIGGER site_aggregates_community_insert + AFTER INSERT OR UPDATE OF removed, + deleted ON community + FOR EACH ROW + WHEN ((new.local = TRUE)) + EXECUTE FUNCTION site_aggregates_community_insert (); + +CREATE TRIGGER site_aggregates_person_delete + AFTER DELETE ON person + FOR EACH ROW + WHEN ((old.local = TRUE)) + EXECUTE FUNCTION site_aggregates_person_delete (); + +CREATE TRIGGER site_aggregates_person_insert + AFTER INSERT ON person + FOR EACH ROW + WHEN ((new.local = TRUE)) + EXECUTE FUNCTION site_aggregates_person_insert (); + +CREATE TRIGGER site_aggregates_post_delete + AFTER DELETE OR UPDATE OF removed, + deleted ON post + FOR EACH ROW + WHEN ((old.local = TRUE)) + EXECUTE FUNCTION site_aggregates_post_delete (); + +CREATE TRIGGER site_aggregates_post_insert + AFTER INSERT ON post REFERENCING NEW TABLE AS new_post + FOR EACH STATEMENT + EXECUTE FUNCTION site_aggregates_post_insert (); + +CREATE TRIGGER site_aggregates_post_update + AFTER UPDATE OF removed, + deleted ON post + FOR EACH ROW + WHEN ((new.local = TRUE)) + EXECUTE FUNCTION site_aggregates_post_update (); + +CREATE TRIGGER site_aggregates_site + AFTER INSERT OR DELETE ON site + FOR EACH ROW + EXECUTE FUNCTION site_aggregates_site (); + +-- Rank functions +CREATE FUNCTION controversy_rank (upvotes numeric, downvotes numeric) + RETURNS double precision + LANGUAGE plpgsql + IMMUTABLE + AS $$ +BEGIN + IF downvotes <= 0 OR upvotes <= 0 THEN + RETURN 0; + ELSE + RETURN (upvotes + downvotes) * CASE WHEN upvotes > downvotes THEN + downvotes::float / upvotes::float + ELSE + upvotes::float / downvotes::float + END; + END IF; +END; +$$; + +CREATE FUNCTION hot_rank (score numeric, published timestamp with time zone) + RETURNS double precision + LANGUAGE plpgsql + IMMUTABLE PARALLEL SAFE + AS $$ +DECLARE + hours_diff numeric := EXTRACT(EPOCH FROM (now() - published)) / 3600; +BEGIN + -- 24 * 7 = 168, so after a week, it will default to 0. + IF (hours_diff > 0 AND hours_diff < 168) THEN + -- Use greatest(2,score), so that the hot_rank will be positive and not ignored. + RETURN log(greatest (2, score + 2)) / power((hours_diff + 2), 1.8); + ELSE + -- if the post is from the future, set hot score to 0. otherwise you can game the post to + -- always be on top even with only 1 vote by setting it to the future + RETURN 0.0; + END IF; +END; +$$; + +CREATE FUNCTION scaled_rank (score numeric, published timestamp with time zone, users_active_month numeric) + RETURNS double precision + LANGUAGE plpgsql + IMMUTABLE PARALLEL SAFE + AS $$ +BEGIN + -- Add 2 to avoid divide by zero errors + -- Default for score = 1, active users = 1, and now, is (0.1728 / log(2 + 1)) = 0.3621 + -- There may need to be a scale factor multiplied to users_active_month, to make + -- the log curve less pronounced. This can be tuned in the future. + RETURN (hot_rank (score, published) / log(2 + users_active_month)); +END; +$$; + +-- Don't defer constraints +ALTER TABLE comment_aggregates + ALTER CONSTRAINT comment_aggregates_comment_id_fkey NOT DEFERRABLE; + +ALTER TABLE community_aggregates + ALTER CONSTRAINT community_aggregates_community_id_fkey NOT DEFERRABLE; + +ALTER TABLE person_aggregates + ALTER CONSTRAINT person_aggregates_person_id_fkey NOT DEFERRABLE; + +ALTER TABLE post_aggregates + ALTER CONSTRAINT post_aggregates_community_id_fkey NOT DEFERRABLE, + ALTER CONSTRAINT post_aggregates_creator_id_fkey NOT DEFERRABLE, + ALTER CONSTRAINT post_aggregates_instance_id_fkey NOT DEFERRABLE, + ALTER CONSTRAINT post_aggregates_post_id_fkey NOT DEFERRABLE; + +ALTER TABLE site_aggregates + ALTER CONSTRAINT site_aggregates_site_id_fkey NOT DEFERRABLE; + diff --git a/migrations/2024-02-24-034523_replaceable-schema/up.sql b/migrations/2024-02-24-034523_replaceable-schema/up.sql new file mode 100644 index 000000000..05e57afc0 --- /dev/null +++ b/migrations/2024-02-24-034523_replaceable-schema/up.sql @@ -0,0 +1,81 @@ +CREATE UNIQUE INDEX idx_site_aggregates_1_row_only ON site_aggregates ((TRUE)); + +-- Drop functions and use `CASCADE` to drop the triggers that use them +DROP FUNCTION comment_aggregates_comment, comment_aggregates_score, community_aggregates_comment_count, community_aggregates_community, community_aggregates_post_count, community_aggregates_post_count_insert, community_aggregates_subscriber_count, delete_follow_before_person, person_aggregates_comment_count, person_aggregates_comment_score, person_aggregates_person, person_aggregates_post_count, person_aggregates_post_insert, person_aggregates_post_score, post_aggregates_comment_count, post_aggregates_featured_community, post_aggregates_featured_local, post_aggregates_post, post_aggregates_score, site_aggregates_comment_delete, site_aggregates_comment_insert, site_aggregates_community_delete, site_aggregates_community_insert, site_aggregates_person_delete, site_aggregates_person_insert, site_aggregates_post_delete, site_aggregates_post_insert, site_aggregates_post_update, site_aggregates_site, was_removed_or_deleted, was_restored_or_created CASCADE; + +-- Drop rank functions +DROP FUNCTION controversy_rank, scaled_rank, hot_rank; + +-- Defer constraints +ALTER TABLE comment_aggregates + ALTER CONSTRAINT comment_aggregates_comment_id_fkey INITIALLY DEFERRED; + +ALTER TABLE community_aggregates + ALTER CONSTRAINT community_aggregates_community_id_fkey INITIALLY DEFERRED; + +ALTER TABLE person_aggregates + ALTER CONSTRAINT person_aggregates_person_id_fkey INITIALLY DEFERRED; + +ALTER TABLE post_aggregates + ALTER CONSTRAINT post_aggregates_community_id_fkey INITIALLY DEFERRED, + ALTER CONSTRAINT post_aggregates_creator_id_fkey INITIALLY DEFERRED, + ALTER CONSTRAINT post_aggregates_instance_id_fkey INITIALLY DEFERRED, + ALTER CONSTRAINT post_aggregates_post_id_fkey INITIALLY DEFERRED; + +ALTER TABLE site_aggregates + ALTER CONSTRAINT site_aggregates_site_id_fkey INITIALLY DEFERRED; + +-- Fix values that might be incorrect because of the old triggers +UPDATE + post_aggregates +SET + featured_local = post.featured_local, + featured_community = post.featured_community +FROM + post +WHERE + post_aggregates.post_id = post.id + AND (post_aggregates.featured_local, + post_aggregates.featured_community) != (post.featured_local, + post.featured_community); + +UPDATE + community_aggregates +SET + comments = counted.comments +FROM ( + SELECT + community_id, + count(*) AS comments + FROM + comment, + LATERAL ( + SELECT + * + FROM + post + WHERE + post.id = comment.post_id + LIMIT 1) AS post + WHERE + NOT (comment.deleted + OR comment.removed + OR post.deleted + OR post.removed) + GROUP BY + community_id) AS counted +WHERE + community_aggregates.community_id = counted.community_id + AND community_aggregates.comments != counted.comments; + +UPDATE + site_aggregates +SET + communities = ( + SELECT + count(*) + FROM + community + WHERE + local); + diff --git a/migrations/2024-04-15-105932_community_followers_url_optional/down.sql b/migrations/2024-04-15-105932_community_followers_url_optional/down.sql new file mode 100644 index 000000000..68ad34377 --- /dev/null +++ b/migrations/2024-04-15-105932_community_followers_url_optional/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE community + ALTER COLUMN followers_url SET NOT NULL; + diff --git a/migrations/2024-04-15-105932_community_followers_url_optional/up.sql b/migrations/2024-04-15-105932_community_followers_url_optional/up.sql new file mode 100644 index 000000000..3b0ddaec1 --- /dev/null +++ b/migrations/2024-04-15-105932_community_followers_url_optional/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE community + ALTER COLUMN followers_url DROP NOT NULL; + diff --git a/migrations/2024-04-23-020604_add_post_id_index/down.sql b/migrations/2024-04-23-020604_add_post_id_index/down.sql new file mode 100644 index 000000000..fb4c92f6c --- /dev/null +++ b/migrations/2024-04-23-020604_add_post_id_index/down.sql @@ -0,0 +1,120 @@ +DROP INDEX idx_post_aggregates_community_active; + +DROP INDEX idx_post_aggregates_community_controversy; + +DROP INDEX idx_post_aggregates_community_hot; + +DROP INDEX idx_post_aggregates_community_most_comments; + +DROP INDEX idx_post_aggregates_community_newest_comment_time; + +DROP INDEX idx_post_aggregates_community_newest_comment_time_necro; + +DROP INDEX idx_post_aggregates_community_published; + +DROP INDEX idx_post_aggregates_community_published_asc; + +DROP INDEX idx_post_aggregates_community_scaled; + +DROP INDEX idx_post_aggregates_community_score; + +DROP INDEX idx_post_aggregates_featured_community_active; + +DROP INDEX idx_post_aggregates_featured_community_controversy; + +DROP INDEX idx_post_aggregates_featured_community_hot; + +DROP INDEX idx_post_aggregates_featured_community_most_comments; + +DROP INDEX idx_post_aggregates_featured_community_newest_comment_time; + +DROP INDEX idx_post_aggregates_featured_community_newest_comment_time_necr; + +DROP INDEX idx_post_aggregates_featured_community_published; + +DROP INDEX idx_post_aggregates_featured_community_published_asc; + +DROP INDEX idx_post_aggregates_featured_community_scaled; + +DROP INDEX idx_post_aggregates_featured_community_score; + +DROP INDEX idx_post_aggregates_featured_local_active; + +DROP INDEX idx_post_aggregates_featured_local_controversy; + +DROP INDEX idx_post_aggregates_featured_local_hot; + +DROP INDEX idx_post_aggregates_featured_local_most_comments; + +DROP INDEX idx_post_aggregates_featured_local_newest_comment_time; + +DROP INDEX idx_post_aggregates_featured_local_newest_comment_time_necro; + +DROP INDEX idx_post_aggregates_featured_local_published; + +DROP INDEX idx_post_aggregates_featured_local_published_asc; + +DROP INDEX idx_post_aggregates_featured_local_scaled; + +DROP INDEX idx_post_aggregates_featured_local_score; + +CREATE INDEX idx_post_aggregates_community_active ON public.post_aggregates USING btree (community_id, featured_local DESC, hot_rank_active DESC, published DESC); + +CREATE INDEX idx_post_aggregates_community_controversy ON public.post_aggregates USING btree (community_id, featured_local DESC, controversy_rank DESC); + +CREATE INDEX idx_post_aggregates_community_hot ON public.post_aggregates USING btree (community_id, featured_local DESC, hot_rank DESC, published DESC); + +CREATE INDEX idx_post_aggregates_community_most_comments ON public.post_aggregates USING btree (community_id, featured_local DESC, comments DESC, published DESC); + +CREATE INDEX idx_post_aggregates_community_newest_comment_time ON public.post_aggregates USING btree (community_id, featured_local DESC, newest_comment_time DESC); + +CREATE INDEX idx_post_aggregates_community_newest_comment_time_necro ON public.post_aggregates USING btree (community_id, featured_local DESC, newest_comment_time_necro DESC); + +CREATE INDEX idx_post_aggregates_community_published ON public.post_aggregates USING btree (community_id, featured_local DESC, published DESC); + +CREATE INDEX idx_post_aggregates_community_published_asc ON public.post_aggregates USING btree (community_id, featured_local DESC, public.reverse_timestamp_sort (published) DESC); + +CREATE INDEX idx_post_aggregates_community_scaled ON public.post_aggregates USING btree (community_id, featured_local DESC, scaled_rank DESC, published DESC); + +CREATE INDEX idx_post_aggregates_community_score ON public.post_aggregates USING btree (community_id, featured_local DESC, score DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_community_active ON public.post_aggregates USING btree (community_id, featured_community DESC, hot_rank_active DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_community_controversy ON public.post_aggregates USING btree (community_id, featured_community DESC, controversy_rank DESC); + +CREATE INDEX idx_post_aggregates_featured_community_hot ON public.post_aggregates USING btree (community_id, featured_community DESC, hot_rank DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_community_most_comments ON public.post_aggregates USING btree (community_id, featured_community DESC, comments DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_community_newest_comment_time ON public.post_aggregates USING btree (community_id, featured_community DESC, newest_comment_time DESC); + +CREATE INDEX idx_post_aggregates_featured_community_newest_comment_time_necr ON public.post_aggregates USING btree (community_id, featured_community DESC, newest_comment_time_necro DESC); + +CREATE INDEX idx_post_aggregates_featured_community_published ON public.post_aggregates USING btree (community_id, featured_community DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_community_published_asc ON public.post_aggregates USING btree (community_id, featured_community DESC, public.reverse_timestamp_sort (published) DESC); + +CREATE INDEX idx_post_aggregates_featured_community_scaled ON public.post_aggregates USING btree (community_id, featured_community DESC, scaled_rank DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_community_score ON public.post_aggregates USING btree (community_id, featured_community DESC, score DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_local_active ON public.post_aggregates USING btree (featured_local DESC, hot_rank_active DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_local_controversy ON public.post_aggregates USING btree (featured_local DESC, controversy_rank DESC); + +CREATE INDEX idx_post_aggregates_featured_local_hot ON public.post_aggregates USING btree (featured_local DESC, hot_rank DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_local_most_comments ON public.post_aggregates USING btree (featured_local DESC, comments DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_local_newest_comment_time ON public.post_aggregates USING btree (featured_local DESC, newest_comment_time DESC); + +CREATE INDEX idx_post_aggregates_featured_local_newest_comment_time_necro ON public.post_aggregates USING btree (featured_local DESC, newest_comment_time_necro DESC); + +CREATE INDEX idx_post_aggregates_featured_local_published ON public.post_aggregates USING btree (featured_local DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_local_published_asc ON public.post_aggregates USING btree (featured_local DESC, public.reverse_timestamp_sort (published) DESC); + +CREATE INDEX idx_post_aggregates_featured_local_scaled ON public.post_aggregates USING btree (featured_local DESC, scaled_rank DESC, published DESC); + +CREATE INDEX idx_post_aggregates_featured_local_score ON public.post_aggregates USING btree (featured_local DESC, score DESC, published DESC); + diff --git a/migrations/2024-04-23-020604_add_post_id_index/up.sql b/migrations/2024-04-23-020604_add_post_id_index/up.sql new file mode 100644 index 000000000..8581d7169 --- /dev/null +++ b/migrations/2024-04-23-020604_add_post_id_index/up.sql @@ -0,0 +1,121 @@ +-- Add , post_id DESC to all these +DROP INDEX idx_post_aggregates_community_active; + +DROP INDEX idx_post_aggregates_community_controversy; + +DROP INDEX idx_post_aggregates_community_hot; + +DROP INDEX idx_post_aggregates_community_most_comments; + +DROP INDEX idx_post_aggregates_community_newest_comment_time; + +DROP INDEX idx_post_aggregates_community_newest_comment_time_necro; + +DROP INDEX idx_post_aggregates_community_published; + +DROP INDEX idx_post_aggregates_community_published_asc; + +DROP INDEX idx_post_aggregates_community_scaled; + +DROP INDEX idx_post_aggregates_community_score; + +DROP INDEX idx_post_aggregates_featured_community_active; + +DROP INDEX idx_post_aggregates_featured_community_controversy; + +DROP INDEX idx_post_aggregates_featured_community_hot; + +DROP INDEX idx_post_aggregates_featured_community_most_comments; + +DROP INDEX idx_post_aggregates_featured_community_newest_comment_time; + +DROP INDEX idx_post_aggregates_featured_community_newest_comment_time_necr; + +DROP INDEX idx_post_aggregates_featured_community_published; + +DROP INDEX idx_post_aggregates_featured_community_published_asc; + +DROP INDEX idx_post_aggregates_featured_community_scaled; + +DROP INDEX idx_post_aggregates_featured_community_score; + +DROP INDEX idx_post_aggregates_featured_local_active; + +DROP INDEX idx_post_aggregates_featured_local_controversy; + +DROP INDEX idx_post_aggregates_featured_local_hot; + +DROP INDEX idx_post_aggregates_featured_local_most_comments; + +DROP INDEX idx_post_aggregates_featured_local_newest_comment_time; + +DROP INDEX idx_post_aggregates_featured_local_newest_comment_time_necro; + +DROP INDEX idx_post_aggregates_featured_local_published; + +DROP INDEX idx_post_aggregates_featured_local_published_asc; + +DROP INDEX idx_post_aggregates_featured_local_scaled; + +DROP INDEX idx_post_aggregates_featured_local_score; + +CREATE INDEX idx_post_aggregates_community_active ON public.post_aggregates USING btree (community_id, featured_local DESC, hot_rank_active DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_controversy ON public.post_aggregates USING btree (community_id, featured_local DESC, controversy_rank DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_hot ON public.post_aggregates USING btree (community_id, featured_local DESC, hot_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_most_comments ON public.post_aggregates USING btree (community_id, featured_local DESC, comments DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_newest_comment_time ON public.post_aggregates USING btree (community_id, featured_local DESC, newest_comment_time DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_newest_comment_time_necro ON public.post_aggregates USING btree (community_id, featured_local DESC, newest_comment_time_necro DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_published ON public.post_aggregates USING btree (community_id, featured_local DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_published_asc ON public.post_aggregates USING btree (community_id, featured_local DESC, public.reverse_timestamp_sort (published) DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_scaled ON public.post_aggregates USING btree (community_id, featured_local DESC, scaled_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_score ON public.post_aggregates USING btree (community_id, featured_local DESC, score DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_active ON public.post_aggregates USING btree (community_id, featured_community DESC, hot_rank_active DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_controversy ON public.post_aggregates USING btree (community_id, featured_community DESC, controversy_rank DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_hot ON public.post_aggregates USING btree (community_id, featured_community DESC, hot_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_most_comments ON public.post_aggregates USING btree (community_id, featured_community DESC, comments DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_newest_comment_time ON public.post_aggregates USING btree (community_id, featured_community DESC, newest_comment_time DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_newest_comment_time_necr ON public.post_aggregates USING btree (community_id, featured_community DESC, newest_comment_time_necro DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_published ON public.post_aggregates USING btree (community_id, featured_community DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_published_asc ON public.post_aggregates USING btree (community_id, featured_community DESC, public.reverse_timestamp_sort (published) DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_scaled ON public.post_aggregates USING btree (community_id, featured_community DESC, scaled_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_score ON public.post_aggregates USING btree (community_id, featured_community DESC, score DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_active ON public.post_aggregates USING btree (featured_local DESC, hot_rank_active DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_controversy ON public.post_aggregates USING btree (featured_local DESC, controversy_rank DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_hot ON public.post_aggregates USING btree (featured_local DESC, hot_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_most_comments ON public.post_aggregates USING btree (featured_local DESC, comments DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_newest_comment_time ON public.post_aggregates USING btree (featured_local DESC, newest_comment_time DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_newest_comment_time_necro ON public.post_aggregates USING btree (featured_local DESC, newest_comment_time_necro DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_published ON public.post_aggregates USING btree (featured_local DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_published_asc ON public.post_aggregates USING btree (featured_local DESC, public.reverse_timestamp_sort (published) DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_scaled ON public.post_aggregates USING btree (featured_local DESC, scaled_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_score ON public.post_aggregates USING btree (featured_local DESC, score DESC, published DESC, post_id DESC); + diff --git a/migrations/2024-05-04-140749_separate_triggers/down.sql b/migrations/2024-05-04-140749_separate_triggers/down.sql new file mode 100644 index 000000000..deb75def2 --- /dev/null +++ b/migrations/2024-05-04-140749_separate_triggers/down.sql @@ -0,0 +1,3 @@ +SELECT + 1; + diff --git a/migrations/2024-05-04-140749_separate_triggers/up.sql b/migrations/2024-05-04-140749_separate_triggers/up.sql new file mode 100644 index 000000000..2e9d05052 --- /dev/null +++ b/migrations/2024-05-04-140749_separate_triggers/up.sql @@ -0,0 +1,4 @@ +-- This migration exists to trigger re-execution of replaceable_schema +SELECT + 1; + diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..1911d651b --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "schedule": ["before 4am on the first day of the month"] +} diff --git a/scripts/dump_schema.sh b/scripts/dump_schema.sh new file mode 100755 index 000000000..f783be26b --- /dev/null +++ b/scripts/dump_schema.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -e + +# Dumps database schema, not including things that are added outside of migrations + +CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" + +cd $CWD/../ + +source scripts/start_dev_db.sh + +diesel migration run +pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f schema.sqldump + +pg_ctl stop +rm -rf $PGDATA diff --git a/scripts/postgres_15_to_16_upgrade.sh b/scripts/postgres_15_to_16_upgrade.sh index 20a6c594c..f2ac0d5d6 100755 --- a/scripts/postgres_15_to_16_upgrade.sh +++ b/scripts/postgres_15_to_16_upgrade.sh @@ -4,39 +4,39 @@ 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 +sudo docker compose stop echo "Make sure postgres is started..." -sudo docker-compose up -d postgres +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 > 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 +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." -sed -i "s/image: postgres:.*/image: postgres:16-alpine/" ./docker-compose.yml +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 +sudo docker compose up -d postgres echo "Waiting..." sleep 20s echo "Importing the database...." -cat 15_16_dump.sql | sudo docker-compose exec -T postgres psql -U lemmy +sudo cat 15_16_dump.sql | sudo docker compose exec -T postgres psql -U lemmy echo "Done." echo "Starting up lemmy..." -sudo docker-compose up -d +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." diff --git a/scripts/sql_format_check.sh b/scripts/sql_format_check.sh index a75425da8..fabc3b3ed 100755 --- a/scripts/sql_format_check.sh +++ b/scripts/sql_format_check.sh @@ -9,10 +9,12 @@ cd $CWD/../ # Copy the files to a temp dir TMP_DIR=$(mktemp -d) -cp -a migrations/. $TMP_DIR +cp -a migrations/. $TMP_DIR/migrations +cp -a crates/db_schema/replaceable_schema/. $TMP_DIR/replaceable_schema # Format the new files find $TMP_DIR -type f -name '*.sql' -exec pg_format -i {} + # Diff the directories -diff -r migrations $TMP_DIR +diff -r migrations $TMP_DIR/migrations +diff -r crates/db_schema/replaceable_schema $TMP_DIR/replaceable_schema diff --git a/scripts/start_dev_db.sh b/scripts/start_dev_db.sh index 8ea4a294e..5965316ba 100644 --- a/scripts/start_dev_db.sh +++ b/scripts/start_dev_db.sh @@ -2,8 +2,10 @@ export PGDATA="$PWD/dev_pgdata" export PGHOST=$PWD +export PGUSER=postgres export DATABASE_URL="postgresql://lemmy:password@/lemmy?host=$PWD" export LEMMY_DATABASE_URL=$DATABASE_URL +export PGDATABASE=lemmy # If cluster exists, stop the server and delete the cluster if [[ -d $PGDATA ]] @@ -44,5 +46,5 @@ pg_ctl init --silent --options="--username=postgres --auth=trust --no-instructio pg_ctl start --silent --options="${config_args[*]}" # Setup database -psql --quiet -c "CREATE USER lemmy WITH PASSWORD 'password' SUPERUSER;" -U postgres -psql --quiet -c "CREATE DATABASE lemmy WITH OWNER lemmy;" -U postgres +PGDATABASE=postgres psql --quiet -c "CREATE USER lemmy WITH PASSWORD 'password' SUPERUSER;" +PGDATABASE=postgres psql --quiet -c "CREATE DATABASE lemmy WITH OWNER lemmy;" diff --git a/scripts/test.sh b/scripts/test.sh index 3e0581fc7..9bb6acaa8 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -20,11 +20,10 @@ then cargo test -p $PACKAGE --all-features --no-fail-fast $TEST else cargo test --workspace --no-fail-fast + # Testing lemmy utils all features in particular (for ts-rs bindings) + cargo test -p lemmy_utils --all-features --no-fail-fast fi -# Testing lemmy utils all features in particular (for ts-rs bindings) -cargo test -p lemmy_utils --all-features --no-fail-fast - # Add this to do printlns: -- --nocapture pg_ctl stop --silent diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 013e2e092..2a15b4ca6 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -262,12 +262,28 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { // User .service( // Account action, I don't like that it's in /user maybe /accounts - // Handle /user/register separately to add the register() rate limitter + // Handle /user/register separately to add the register() rate limiter web::resource("/user/register") .guard(guard::Post()) .wrap(rate_limit.register()) .route(web::post().to(register)), ) + // User + .service( + // Handle /user/login separately to add the register() rate limiter + // TODO: pretty annoying way to apply rate limits for register and login, we should + // group them under a common path so that rate limit is only applied once (eg under + // /account). + web::resource("/user/login") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(web::post().to(login)), + ) + .service( + web::resource("/user/password_reset") + .wrap(rate_limit.register()) + .route(web::post().to(reset_password)), + ) .service( // Handle captcha separately web::resource("/user/get_captcha") @@ -306,10 +322,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/banned", web::get().to(list_banned_users)) .route("/block", web::post().to(block_person)) // TODO Account actions. I don't like that they're in /user maybe /accounts - .route("/login", web::post().to(login)) .route("/logout", web::post().to(logout)) .route("/delete_account", web::post().to(delete_account)) - .route("/password_reset", web::post().to(reset_password)) .route( "/password_change", web::post().to(change_password_after_reset), diff --git a/src/code_migrations.rs b/src/code_migrations.rs index a2325971b..05b564f47 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -345,7 +345,7 @@ async fn instance_actor_2022_01_28( settings: &Settings, ) -> LemmyResult<()> { info!("Running instance_actor_2021_09_29"); - if let Ok(site_view) = SiteView::read_local(pool).await { + if let Ok(Some(site_view)) = SiteView::read_local(pool).await { let site = site_view.site; // if site already has public key, we dont need to do anything here if !site.public_key.is_empty() { diff --git a/src/lib.rs b/src/lib.rs index e9ac06e62..c51072bcc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ use lemmy_apub::{ FEDERATION_HTTP_FETCH_LIMIT, }; use lemmy_db_schema::{source::secret::Secret, utils::build_db_pool}; -use lemmy_federate::{start_stop_federation_workers_cancellable, Opts}; +use lemmy_federate::{Opts, SendManager}; use lemmy_routes::{feeds, images, nodeinfo, webfinger}; use lemmy_utils::{ error::LemmyResult, @@ -70,38 +70,42 @@ use url::Url; about = "A link aggregator for the fediverse", long_about = "A link aggregator for the fediverse.\n\nThis is the Lemmy backend API server. This will connect to a PostgreSQL database, run any pending migrations and start accepting API requests." )] +// TODO: Instead of defining individual env vars, only specify prefix once supported by clap. +// https://github.com/clap-rs/clap/issues/3221 pub struct CmdArgs { /// Don't run scheduled tasks. /// - /// If you are running multiple Lemmy server processes, you probably want to disable scheduled tasks on - /// all but one of the processes, to avoid running the tasks more often than intended. - #[arg(long, default_value_t = false)] + /// If you are running multiple Lemmy server processes, you probably want to disable scheduled + /// tasks on all but one of the processes, to avoid running the tasks more often than intended. + #[arg(long, default_value_t = false, env = "LEMMY_DISABLE_SCHEDULED_TASKS")] disable_scheduled_tasks: bool, /// Disables the HTTP server. /// - /// This can be used to run a Lemmy server process that only performs scheduled tasks or activity sending. - #[arg(long, default_value_t = false)] + /// This can be used to run a Lemmy server process that only performs scheduled tasks or activity + /// sending. + #[arg(long, default_value_t = false, env = "LEMMY_DISABLE_HTTP_SERVER")] disable_http_server: bool, /// Disable sending outgoing ActivityPub messages. /// /// Only pass this for horizontally scaled setups. /// See https://join-lemmy.org/docs/administration/horizontal_scaling.html for details. - #[arg(long, default_value_t = false)] + #[arg(long, default_value_t = false, env = "LEMMY_DISABLE_ACTIVITY_SENDING")] disable_activity_sending: bool, /// The index of this outgoing federation process. /// - /// Defaults to 1/1. If you want to split the federation workload onto n servers, run each server 1≤i≤n with these args: - /// --federate-process-index i --federate-process-count n + /// Defaults to 1/1. If you want to split the federation workload onto n servers, run each server + /// 1≤i≤n with these args: --federate-process-index i --federate-process-count n /// - /// Make you have exactly one server with each `i` running, otherwise federation will randomly send duplicates or nothing. + /// Make you have exactly one server with each `i` running, otherwise federation will randomly + /// send duplicates or nothing. /// /// See https://join-lemmy.org/docs/administration/horizontal_scaling.html for more detail. - #[arg(long, default_value_t = 1)] + #[arg(long, default_value_t = 1, env = "LEMMY_FEDERATE_PROCESS_INDEX")] federate_process_index: i32, /// How many outgoing federation processes you are starting in total. /// /// If set, make sure to set --federate-process-index differently for each. - #[arg(long, default_value_t = 1)] + #[arg(long, default_value_t = 1, env = "LEMMY_FEDERATE_PROCESS_COUNT")] federate_process_count: i32, } @@ -124,12 +128,12 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { // Initialize the secrets let secret = Secret::init(&mut (&pool).into()) - .await + .await? .expect("Couldn't initialize secrets."); // Make sure the local site is set up. let site_view = SiteView::read_local(&mut (&pool).into()) - .await + .await? .expect("local site not set up"); let local_site = site_view.local_site; let federation_enabled = local_site.federation_enabled; @@ -160,10 +164,10 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { rate_limit_cell.clone(), ); - if !args.disable_scheduled_tasks { + let scheduled_tasks = (!args.disable_scheduled_tasks).then(|| { // Schedules various cleanup tasks for the DB - let _scheduled_tasks = tokio::task::spawn(scheduled_tasks::setup(context.clone())); - } + tokio::task::spawn(scheduled_tasks::setup(context.clone())) + }); if let Some(prometheus) = SETTINGS.prometheus.clone() { serve_prometheus(prometheus, context.clone())?; @@ -206,28 +210,29 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { None }; let federate = (!args.disable_activity_sending).then(|| { - start_stop_federation_workers_cancellable( + SendManager::run( Opts { process_index: args.federate_process_index, process_count: args.federate_process_count, }, - pool.clone(), - federation_config.clone(), + federation_config, SETTINGS.federation.clone(), ) }); let mut interrupt = tokio::signal::unix::signal(SignalKind::interrupt())?; let mut terminate = tokio::signal::unix::signal(SignalKind::terminate())?; - tokio::select! { - _ = tokio::signal::ctrl_c() => { - tracing::warn!("Received ctrl-c, shutting down gracefully..."); - } - _ = interrupt.recv() => { - tracing::warn!("Received interrupt, shutting down gracefully..."); - } - _ = terminate.recv() => { - tracing::warn!("Received terminate, shutting down gracefully..."); + if server.is_some() || federate.is_some() || scheduled_tasks.is_some() { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + tracing::warn!("Received ctrl-c, shutting down gracefully..."); + } + _ = interrupt.recv() => { + tracing::warn!("Received interrupt, shutting down gracefully..."); + } + _ = terminate.recv() => { + tracing::warn!("Received terminate, shutting down gracefully..."); + } } } if let Some(server) = server { @@ -289,7 +294,9 @@ fn create_http_server( let cors_config = cors_config(&settings); let app = App::new() .wrap(middleware::Logger::new( - // This is the default log format save for the usage of %{r}a over %a to guarantee to record the client's (forwarded) IP and not the last peer address, since the latter is frequently just a reverse proxy + // This is the default log format save for the usage of %{r}a over %a to guarantee to + // record the client's (forwarded) IP and not the last peer address, since the latter is + // frequently just a reverse proxy "%{r}a '%r' %s %b '%{Referer}i' '%{User-Agent}i' %T", )) .wrap(middleware::Compress::default()) @@ -331,7 +338,8 @@ fn cors_config(settings: &Settings) -> Cors { let cors_origin_setting = settings.cors_origin(); match (cors_origin_setting.clone(), cfg!(debug_assertions)) { (Some(origin), false) => { - // Need to call send_wildcard() explicitly, passing this into allowed_origin() results in error + // Need to call send_wildcard() explicitly, passing this into allowed_origin() results in + // error if cors_origin_setting.as_deref() == Some("*") { Cors::default().allow_any_origin().send_wildcard() } else { diff --git a/src/root_span_builder.rs b/src/root_span_builder.rs index e062ade21..09ce2544e 100644 --- a/src/root_span_builder.rs +++ b/src/root_span_builder.rs @@ -79,5 +79,5 @@ fn handle_error(span: Span, status_code: StatusCode, response_error: &dyn Respon } }); - span.record("exception.message", &tracing::field::display(display_error)); + span.record("exception.message", tracing::field::display(display_error)); } diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 8c4cf9311..e591842c6 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -131,7 +131,7 @@ async fn update_hot_ranks(pool: &mut DbPool<'_>) { &mut conn, "comment", "a.hot_rank != 0", - "SET hot_rank = hot_rank(a.score, a.published)", + "SET hot_rank = r.hot_rank(a.score, a.published)", ) .await; @@ -139,7 +139,7 @@ async fn update_hot_ranks(pool: &mut DbPool<'_>) { &mut conn, "community", "a.hot_rank != 0", - "SET hot_rank = hot_rank(a.subscribers, a.published)", + "SET hot_rank = r.hot_rank(a.subscribers, a.published)", ) .await; @@ -236,9 +236,9 @@ async fn process_post_aggregates_ranks_in_batches(conn: &mut AsyncPgConnection) LIMIT $2 FOR UPDATE SKIP LOCKED) UPDATE post_aggregates pa - SET hot_rank = hot_rank(pa.score, pa.published), - hot_rank_active = hot_rank(pa.score, pa.newest_comment_time_necro), - scaled_rank = scaled_rank(pa.score, pa.published, ca.users_active_month) + SET hot_rank = r.hot_rank(pa.score, pa.published), + hot_rank_active = r.hot_rank(pa.score, pa.newest_comment_time_necro), + scaled_rank = r.scaled_rank(pa.score, pa.published, ca.users_active_month) FROM batch, community_aggregates ca WHERE pa.post_id = batch.post_id and pa.community_id = ca.community_id RETURNING pa.published; "#, @@ -467,17 +467,18 @@ async fn update_instance_software( for instance in instances { let node_info_url = format!("https://{}/nodeinfo/2.0.json", instance.domain); - // The `updated` column is used to check if instances are alive. If it is more than three days - // in the past, no outgoing activities will be sent to that instance. However not every - // Fediverse instance has a valid Nodeinfo endpoint (its not required for Activitypub). That's - // why we always need to mark instances as updated if they are alive. + // The `updated` column is used to check if instances are alive. If it is more than three + // days in the past, no outgoing activities will be sent to that instance. However + // not every Fediverse instance has a valid Nodeinfo endpoint (its not required for + // Activitypub). That's why we always need to mark instances as updated if they are + // alive. let default_form = InstanceForm::builder() .domain(instance.domain.clone()) .updated(Some(naive_now())) .build(); let form = match client.get(&node_info_url).send().await { Ok(res) if res.status().is_client_error() => { - // Instance doesnt have nodeinfo but sent a response, consider it alive + // Instance doesn't have nodeinfo but sent a response, consider it alive Some(default_form) } Ok(res) => match res.json::().await { diff --git a/src/session_middleware.rs b/src/session_middleware.rs index 2bee64ca0..f4535249c 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -130,7 +130,7 @@ mod tests { let pool_ = build_db_pool_for_tests().await; let pool = &mut (&pool_).into(); - let secret = Secret::init(pool).await.unwrap(); + let secret = Secret::init(pool).await.unwrap().unwrap(); let context = LemmyContext::create( pool_.clone(), ClientBuilder::new(Client::default()).build(),