diff --git a/.woodpecker.yml b/.woodpecker.yml index 060cc3e26..ded66b557 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -91,6 +91,36 @@ steps: when: - event: pull_request + cargo_clippy: + image: *rust_image + environment: + CARGO_HOME: .cargo_home + commands: + - rustup component add clippy + - cargo clippy --workspace --tests --all-targets -- -D warnings + when: *slow_check_paths + + cargo_test: + image: *rust_image + environment: + LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy + RUST_BACKTRACE: "1" + CARGO_HOME: .cargo_home + LEMMY_TEST_FAST_FEDERATION: "1" + LEMMY_CONFIG_LOCATION: ../../config/config.hjson + commands: + - cargo test --workspace --no-fail-fast + when: *slow_check_paths + + check_ts_bindings: + image: *rust_image + environment: + CARGO_HOME: .cargo_home + commands: + - ./scripts/ts_bindings_check.sh + when: + - event: pull_request + # make sure api builds with default features (used by other crates relying on lemmy api) check_api_common_default_features: image: *rust_image @@ -138,26 +168,6 @@ steps: - diff tmp.schema crates/db_schema/src/schema.rs when: *slow_check_paths - check_db_perf_tool: - image: *rust_image - environment: - LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy - RUST_BACKTRACE: "1" - CARGO_HOME: .cargo_home - commands: - # same as scripts/db_perf.sh but without creating a new database server - - cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1 - when: *slow_check_paths - - cargo_clippy: - image: *rust_image - environment: - CARGO_HOME: .cargo_home - commands: - - rustup component add clippy - - cargo clippy --workspace --tests --all-targets -- -D warnings - when: *slow_check_paths - cargo_build: image: *rust_image environment: @@ -167,27 +177,6 @@ steps: - mv target/debug/lemmy_server target/lemmy_server when: *slow_check_paths - cargo_test: - image: *rust_image - environment: - LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy - RUST_BACKTRACE: "1" - CARGO_HOME: .cargo_home - LEMMY_TEST_FAST_FEDERATION: "1" - LEMMY_CONFIG_LOCATION: ../../config/config.hjson - commands: - - cargo test --workspace --no-fail-fast - when: *slow_check_paths - - check_ts_bindings: - image: *rust_image - environment: - CARGO_HOME: .cargo_home - commands: - - ./scripts/ts_bindings_check.sh - when: - - event: pull_request - check_diesel_migration: # TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server image: *rust_image @@ -221,6 +210,17 @@ steps: - diff before.sqldump after.sqldump when: *slow_check_paths + check_db_perf_tool: + image: *rust_image + environment: + LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy + RUST_BACKTRACE: "1" + CARGO_HOME: .cargo_home + commands: + # same as scripts/db_perf.sh but without creating a new database server + - cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1 + when: *slow_check_paths + run_federation_tests: image: node:22-bookworm-slim environment: @@ -280,24 +280,26 @@ steps: # using https://github.com/pksunkara/cargo-workspaces publish_to_crates_io: image: *rust_image + environment: + CARGO_API_TOKEN: + from_secret: cargo_api_token commands: - *install_binstall # Install cargo-workspaces - cargo binstall -y cargo-workspaces - cp -r migrations crates/db_schema/ - cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}" - secrets: [cargo_api_token] when: - event: tag - notify_on_failure: + notify_on_build: image: alpine:3 commands: - apk add curl - - "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci" + - "curl -d'Lemmy CI build ${CI_PIPELINE_STATUS}: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci" when: - event: [pull_request, tag] - status: failure + status: [failure, success] notify_on_tag_deploy: image: alpine:3 diff --git a/Cargo.lock b/Cargo.lock index 99b355ace..165353be0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "accept-language" @@ -10,9 +10,9 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772" [[package]] name = "activitypub_federation" -version = "0.6.0-alpha2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4877d467ddf2fac85e9ee33aba6f2560df14125b8bfa864f85ab40e9b87753a9" +checksum = "ee819cada736b6e26c59706f9e6ff89a48060e635c0546ff984d84baefc8c13a" dependencies = [ "activitystreams-kinds", "actix-web", @@ -137,7 +137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -272,7 +272,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" dependencies = [ "backtrace", ] @@ -484,13 +484,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -635,7 +635,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.77", + "syn 2.0.87", "which", ] @@ -833,9 +833,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -843,9 +843,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -862,7 +862,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1096,7 +1096,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1118,20 +1118,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.77", -] - -[[package]] -name = "deadpool" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" -dependencies = [ - "async-trait", - "deadpool-runtime", - "num_cpus", - "retain_mut", - "tokio", + "syn 2.0.87", ] [[package]] @@ -1194,7 +1181,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1215,7 +1202,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1225,7 +1212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1238,7 +1225,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1252,9 +1239,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.1.6" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2" +checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" dependencies = [ "bitflags 2.6.0", "byteorder", @@ -1268,12 +1255,12 @@ dependencies = [ [[package]] name = "diesel-async" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acada1517534c92d3f382217b485db8a8638f111b0e3f2a2a8e26165050f77be" +checksum = "4c5c6ec8d5c7b8444d19a47161797cbe361e0fb1ee40c6a8124ec915b64a4125" dependencies = [ "async-trait", - "deadpool 0.9.5", + "deadpool", "diesel", "futures-util", "scoped-futures", @@ -1281,6 +1268,15 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "diesel-bind-if-some" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed8ce9db476124d2eaf4c9db45dc6581b8e8c4c4d47d5e0f39de1fb55dfb2a7" +dependencies = [ + "diesel", +] + [[package]] name = "diesel-derive-enum" version = "2.1.0" @@ -1290,7 +1286,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1301,19 +1297,20 @@ checksum = "d5adf688c584fe33726ce0e2898f608a2a92578ac94a4a92fcecf73214fe0716" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] name = "diesel_derives" -version = "2.1.4" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14701062d6bed917b5c7103bdffaee1e4609279e240488ad24e7bd979ca6866c" +checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" dependencies = [ "diesel_table_macro_syntax", + "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1328,9 +1325,9 @@ dependencies = [ [[package]] name = "diesel_migrations" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" dependencies = [ "diesel", "migrations_internals", @@ -1339,11 +1336,11 @@ dependencies = [ [[package]] name = "diesel_table_macro_syntax" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" dependencies = [ - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1381,7 +1378,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1420,6 +1417,20 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dsl_auto_type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +dependencies = [ + "darling 0.20.10", + "either", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1465,9 +1476,9 @@ checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -1495,7 +1506,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1693,7 +1704,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1905,7 +1916,7 @@ dependencies = [ "markup5ever 0.12.1", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2069,7 +2080,7 @@ dependencies = [ "http 1.1.0", "hyper 1.4.1", "hyper-util", - "rustls 0.23.14", + "rustls 0.23.16", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -2114,7 +2125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8215279f83f9b829403812f845aa2d0dd5966332aa2fd0334a375256f3dd0322" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2255,7 +2266,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2276,24 +2287,23 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "idna" -version = "1.0.2" +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", - "smallvec", - "utf8_iter", ] [[package]] @@ -2497,7 +2507,6 @@ dependencies = [ "encoding_rs", "enum-map", "futures", - "getrandom", "jsonwebtoken", "lemmy_db_schema", "lemmy_db_views", @@ -2505,6 +2514,7 @@ dependencies = [ "lemmy_db_views_moderator", "lemmy_utils", "mime", + "mime_guess", "moka", "pretty_assertions", "regex", @@ -2539,7 +2549,6 @@ dependencies = [ "lemmy_db_views", "lemmy_db_views_actor", "lemmy_utils", - "moka", "serde", "serde_json", "serde_with", @@ -2609,10 +2618,11 @@ dependencies = [ "async-trait", "bcrypt", "chrono", - "deadpool 0.12.1", + "deadpool", "derive-new", "diesel", "diesel-async", + "diesel-bind-if-some", "diesel-derive-enum", "diesel-derive-newtype", "diesel_ltree", @@ -2620,10 +2630,9 @@ dependencies = [ "futures-util", "i-love-jesus", "lemmy_utils", - "moka", "pretty_assertions", "regex", - "rustls 0.23.14", + "rustls 0.23.16", "serde", "serde_json", "serde_with", @@ -2634,6 +2643,7 @@ dependencies = [ "tokio-postgres-rustls", "tracing", "ts-rs", + "tuplex", "url", "uuid", ] @@ -2775,7 +2785,7 @@ dependencies = [ "reqwest 0.12.8", "reqwest-middleware", "reqwest-tracing", - "rustls 0.23.14", + "rustls 0.23.16", "serde_json", "serial_test", "tokio", @@ -2807,6 +2817,7 @@ dependencies = [ "markdown-it-ruby", "markdown-it-sub", "markdown-it-sup", + "moka", "pretty_assertions", "regex", "reqwest-middleware", @@ -2826,9 +2837,9 @@ dependencies = [ [[package]] name = "lettre" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f204773bab09b150320ea1c83db41dc6ee606a4bc36dc1f43005fe7b58ce06" +checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9" dependencies = [ "async-trait", "base64 0.22.1", @@ -2839,12 +2850,12 @@ dependencies = [ "futures-io", "futures-util", "httpdate", - "idna 1.0.2", + "idna 1.0.3", "mime", "nom", "percent-encoding", "quoted_printable", - "rustls 0.23.14", + "rustls 0.23.16", "rustls-pemfile 2.1.3", "rustls-pki-types", "socket2", @@ -3111,9 +3122,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "migrations_internals" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" dependencies = [ "serde", "toml", @@ -3121,9 +3132,9 @@ dependencies = [ [[package]] name = "migrations_macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" dependencies = [ "migrations_internals", "proc-macro2", @@ -3136,6 +3147,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3203,7 +3224,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3515,7 +3536,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3685,7 +3706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3786,6 +3807,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" +dependencies = [ + "encoding_rs", + "memchr", +] + [[package]] name = "quinn" version = "0.11.5" @@ -3797,7 +3828,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.14", + "rustls 0.23.16", "socket2", "thiserror", "tokio", @@ -3814,7 +3845,7 @@ dependencies = [ "rand", "ring", "rustc-hash 2.0.0", - "rustls 0.23.14", + "rustls 0.23.16", "slab", "thiserror", "tinyvec", @@ -3896,7 +3927,7 @@ checksum = "a25d631e41bfb5fdcde1d4e2215f62f7f0afa3ff11e26563765bd6ea1d229aeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3910,9 +3941,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -4025,7 +4056,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.14", + "rustls 0.23.16", "rustls-pemfile 2.1.3", "rustls-pki-types", "serde", @@ -4076,12 +4107,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "retain_mut" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" - [[package]] name = "rgb" version = "0.8.50" @@ -4148,14 +4173,14 @@ dependencies = [ [[package]] name = "rss" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e92048f840d98c6d6dd870af9101610ea9ff413f11f1bcebf4f4c31d96d957" +checksum = "554a62b3dd5450fcbb0435b3db809f9dd3c6e9f5726172408f7ad3b57ed59057" dependencies = [ "atom_syndication", "derive_builder", "never", - "quick-xml 0.36.1", + "quick-xml 0.37.1", ] [[package]] @@ -4212,9 +4237,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.14" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "aws-lc-rs", "log", @@ -4247,9 +4272,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" @@ -4354,29 +4379,29 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "indexmap 2.5.0", "itoa", @@ -4433,14 +4458,14 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] name = "serial_test" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" dependencies = [ "futures", "log", @@ -4452,13 +4477,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4543,9 +4568,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "sitemap-rs" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88cc73a9aac975541c9054e74ceae8d8ee85edc89a322404c275c1d100fffa51" +checksum = "3c4c6ab96128064ba085256d34e205153555b3803094d76e24d406c76f85a2c9" dependencies = [ "chrono", "xml-builder", @@ -4574,7 +4599,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4702,7 +4727,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4724,9 +4749,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -4756,7 +4781,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4852,7 +4877,7 @@ checksum = "78ea17a2dc368aeca6f554343ced1b1e31f76d63683fa8016e5844bd7a5144a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4872,7 +4897,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4949,9 +4974,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -4973,7 +4998,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -5009,7 +5034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" dependencies = [ "ring", - "rustls 0.23.14", + "rustls 0.23.16", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", @@ -5032,7 +5057,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.14", + "rustls 0.23.16", "rustls-pki-types", "tokio", ] @@ -5052,9 +5077,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.8" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -5073,9 +5098,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.15" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap 2.5.0", "serde", @@ -5160,7 +5185,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -5233,7 +5258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -5269,16 +5294,28 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", "termcolor", ] +[[package]] +name = "tuplex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "676ac81d5454c4dcf37955d34fa8626ede3490f744b86ca14a7b90168d2a08aa" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -5332,12 +5369,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", "serde", ] @@ -5380,9 +5417,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "serde", @@ -5459,7 +5496,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -5493,7 +5530,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5813,9 +5850,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -5924,7 +5961,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", "synstructure", ] @@ -5946,7 +5983,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -5966,7 +6003,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", "synstructure", ] @@ -5987,7 +6024,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6009,7 +6046,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8db7b8b8d..b1553be5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,8 @@ unused_self = "deny" unwrap_used = "deny" unimplemented = "deny" unused_async = "deny" +map_err_ignore = "deny" +expect_used = "deny" [workspace.dependencies] lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" } @@ -92,13 +94,13 @@ lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" } lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" } lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" } lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" } -activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [ +activitypub_federation = { version = "0.6.1", default-features = false, features = [ "actix-web", ] } -diesel = "2.1.6" -diesel_migrations = "2.1.0" -diesel-async = "0.4.1" -serde = { version = "1.0.204", features = ["derive"] } +diesel = "2.2.4" +diesel_migrations = "2.2.0" +diesel-async = "0.5.1" +serde = { version = "1.0.215", features = ["derive"] } serde_with = "3.9.0" actix-web = { version = "4.9.0", default-features = false, features = [ "macros", @@ -111,7 +113,7 @@ actix-web = { version = "4.9.0", default-features = false, features = [ tracing = "0.1.40" tracing-actix-web = { version = "0.7.10", default-features = false } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -url = { version = "2.5.2", features = ["serde"] } +url = { version = "2.5.3", features = ["serde"] } reqwest = { version = "0.12.7", default-features = false, features = [ "json", "blocking", @@ -123,24 +125,27 @@ reqwest-tracing = "0.5.3" clokwerk = "0.4.0" doku = { version = "0.21.1", features = ["url-2"] } bcrypt = "0.15.1" -chrono = { version = "0.4.38", features = ["serde"], default-features = false } -serde_json = { version = "1.0.121", features = ["preserve_order"] } +chrono = { version = "0.4.38", features = [ + "serde", + "now", +], default-features = false } +serde_json = { version = "1.0.132", features = ["preserve_order"] } base64 = "0.22.1" -uuid = { version = "1.10.0", features = ["serde", "v4"] } -async-trait = "0.1.81" +uuid = { version = "1.11.0", features = ["serde"] } +async-trait = "0.1.83" captcha = "0.0.9" -anyhow = { version = "1.0.86", features = [ +anyhow = { version = "1.0.93", features = [ "backtrace", ] } # backtrace is on by default on nightly, but not stable rust diesel_ltree = "0.3.1" -serial_test = "3.1.1" -tokio = { version = "1.39.2", features = ["full"] } -regex = "1.10.5" +serial_test = "3.2.0" +tokio = { version = "1.41.1", features = ["full"] } +regex = "1.11.1" diesel-derive-newtype = "2.1.2" diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } strum = { version = "0.26.3", features = ["derive"] } itertools = "0.13.0" -futures = "0.3.30" +futures = "0.3.31" http = "1.1" rosetta-i18n = "0.1.3" ts-rs = { version = "10.0.0", features = [ @@ -149,17 +154,19 @@ ts-rs = { version = "10.0.0", features = [ "no-serde-warnings", "url-impl", ] } -rustls = { version = "0.23.12", features = ["ring"] } -futures-util = "0.3.30" -tokio-postgres = "0.7.11" +rustls = { version = "0.23.16", features = ["ring"] } +futures-util = "0.3.31" +tokio-postgres = "0.7.12" tokio-postgres-rustls = "0.12.0" urlencoding = "2.1.3" enum-map = "2.7" moka = { version = "0.12.8", features = ["future"] } i-love-jesus = { version = "0.1.0" } -clap = { version = "4.5.13", features = ["derive", "env"] } -pretty_assertions = "1.4.0" +clap = { version = "4.5.21", features = ["derive", "env"] } +pretty_assertions = "1.4.1" derive-new = "0.7.0" +diesel-bind-if-some = "0.1.0" +tuplex = "0.1.2" [dependencies] lemmy_api = { workspace = true } diff --git a/api_tests/package.json b/api_tests/package.json index 9a5057c00..ef47bf192 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -22,16 +22,16 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/node": "^22.3.0", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.1.0", - "eslint": "^9.9.0", + "@types/node": "^22.9.0", + "@typescript-eslint/eslint-plugin": "^8.13.0", + "@typescript-eslint/parser": "^8.13.0", + "eslint": "^9.14.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-private-community.9", + "lemmy-js-client": "0.20.0-instance-blocks.5", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", - "typescript-eslint": "^8.1.0" + "typescript-eslint": "^8.13.0" } } diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index b1f18622e..a95e80726 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -12,38 +12,38 @@ importers: specifier: ^29.5.12 version: 29.5.14 '@types/node': - specifier: ^22.3.0 - version: 22.8.6 + specifier: ^22.9.0 + version: 22.9.0 '@typescript-eslint/eslint-plugin': - specifier: ^8.1.0 - version: 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3) + specifier: ^8.13.0 + version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3) '@typescript-eslint/parser': - specifier: ^8.1.0 - version: 8.12.2(eslint@9.13.0)(typescript@5.6.3) + specifier: ^8.13.0 + version: 8.13.0(eslint@9.14.0)(typescript@5.6.3) eslint: - specifier: ^9.9.0 - version: 9.13.0 + specifier: ^9.14.0 + version: 9.14.0 eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.2.1(eslint@9.13.0)(prettier@3.3.3) + version: 5.2.1(eslint@9.14.0)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@22.8.6) + version: 29.7.0(@types/node@22.9.0) lemmy-js-client: - specifier: 0.20.0-private-community.9 - version: 0.20.0-private-community.9 + specifier: 0.20.0-instance-blocks.5 + version: 0.20.0-instance-blocks.5 prettier: specifier: ^3.2.5 version: 3.3.3 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.8.6))(typescript@5.6.3) + version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3) typescript: specifier: ^5.5.4 version: 5.6.3 typescript-eslint: - specifier: ^8.1.0 - version: 8.12.2(eslint@9.13.0)(typescript@5.6.3) + specifier: ^8.13.0 + version: 8.13.0(eslint@9.14.0)(typescript@5.6.3) packages: @@ -240,8 +240,8 @@ packages: resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.13.0': - resolution: {integrity: sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==} + '@eslint/js@9.14.0': + resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.4': @@ -268,6 +268,10 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -418,8 +422,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.8.6': - resolution: {integrity: sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==} + '@types/node@22.9.0': + resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -430,8 +434,8 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@8.12.2': - resolution: {integrity: sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==} + '@typescript-eslint/eslint-plugin@8.13.0': + resolution: {integrity: sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -441,8 +445,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.12.2': - resolution: {integrity: sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==} + '@typescript-eslint/parser@8.13.0': + resolution: {integrity: sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -451,12 +455,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.12.2': - resolution: {integrity: sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==} + '@typescript-eslint/scope-manager@8.13.0': + resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.12.2': - resolution: {integrity: sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==} + '@typescript-eslint/type-utils@8.13.0': + resolution: {integrity: sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -464,12 +468,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.12.2': - resolution: {integrity: sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==} + '@typescript-eslint/types@8.13.0': + resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.12.2': - resolution: {integrity: sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==} + '@typescript-eslint/typescript-estree@8.13.0': + resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -477,14 +481,14 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.12.2': - resolution: {integrity: sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==} + '@typescript-eslint/utils@8.13.0': + resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@typescript-eslint/visitor-keys@8.12.2': - resolution: {integrity: sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==} + '@typescript-eslint/visitor-keys@8.13.0': + resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -649,6 +653,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.5: + resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + engines: {node: '>= 8'} + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -737,8 +745,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.13.0: - resolution: {integrity: sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==} + eslint@9.14.0: + resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1159,8 +1167,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-private-community.9: - resolution: {integrity: sha512-iuFezswCzIco5U5Q4Eo8HAWVE65pDW2zeO+fYLEyFl30SLw9a3gqJkip2vbDfVvoAjDXyUskZKddf1Nnj8mVcg==} + lemmy-js-client@0.20.0-instance-blocks.5: + resolution: {integrity: sha512-wDuRFzg32lbbJr4cNmd+cbzjgw+okw2/d5AujYjAm4gv0OEFfsYhP3QQ2WscwUR5HJTdzsR7IIyiBnvmaEUzUw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -1533,8 +1541,8 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript-eslint@8.12.2: - resolution: {integrity: sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==} + typescript-eslint@8.13.0: + resolution: {integrity: sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1808,9 +1816,9 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.1(eslint@9.13.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0)': dependencies: - eslint: 9.13.0 + eslint: 9.14.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -1839,7 +1847,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.13.0': {} + '@eslint/js@9.14.0': {} '@eslint/object-schema@2.1.4': {} @@ -1858,6 +1866,8 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} + '@humanwhocodes/retry@0.4.1': {} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -1871,7 +1881,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1884,14 +1894,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.8.6) + jest-config: 29.7.0(@types/node@22.9.0) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1916,7 +1926,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1934,7 +1944,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.8.6 + '@types/node': 22.9.0 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1956,7 +1966,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 22.8.6 + '@types/node': 22.9.0 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2026,7 +2036,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.8.6 + '@types/node': 22.9.0 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2096,7 +2106,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.8.6 + '@types/node': 22.9.0 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2115,7 +2125,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@22.8.6': + '@types/node@22.9.0': dependencies: undici-types: 6.19.8 @@ -2127,15 +2137,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.12.2(eslint@9.13.0)(typescript@5.6.3) - '@typescript-eslint/scope-manager': 8.12.2 - '@typescript-eslint/type-utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.12.2 - eslint: 9.13.0 + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.13.0 + eslint: 9.14.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -2145,28 +2155,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3)': + '@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/scope-manager': 8.12.2 - '@typescript-eslint/types': 8.12.2 - '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.12.2 + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.13.0 debug: 4.3.7 - eslint: 9.13.0 + eslint: 9.14.0 optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.12.2': + '@typescript-eslint/scope-manager@8.13.0': dependencies: - '@typescript-eslint/types': 8.12.2 - '@typescript-eslint/visitor-keys': 8.12.2 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/visitor-keys': 8.13.0 - '@typescript-eslint/type-utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)': + '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) - '@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) debug: 4.3.7 ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: @@ -2175,12 +2185,12 @@ snapshots: - eslint - supports-color - '@typescript-eslint/types@8.12.2': {} + '@typescript-eslint/types@8.13.0': {} - '@typescript-eslint/typescript-estree@8.12.2(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@8.13.0(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.12.2 - '@typescript-eslint/visitor-keys': 8.12.2 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/visitor-keys': 8.13.0 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 @@ -2192,20 +2202,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)': + '@typescript-eslint/utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0) - '@typescript-eslint/scope-manager': 8.12.2 - '@typescript-eslint/types': 8.12.2 - '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) - eslint: 9.13.0 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) + eslint: 9.14.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@8.12.2': + '@typescript-eslint/visitor-keys@8.13.0': dependencies: - '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/types': 8.13.0 eslint-visitor-keys: 3.4.3 acorn-jsx@5.3.2(acorn@8.14.0): @@ -2373,13 +2383,13 @@ snapshots: convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@22.8.6): + create-jest@29.7.0(@types/node@22.9.0): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.8.6) + jest-config: 29.7.0(@types/node@22.9.0) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2394,6 +2404,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.5: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + debug@4.3.7: dependencies: ms: 2.1.3 @@ -2428,9 +2444,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.2.1(eslint@9.13.0)(prettier@3.3.3): + eslint-plugin-prettier@5.2.1(eslint@9.14.0)(prettier@3.3.3): dependencies: - eslint: 9.13.0 + eslint: 9.14.0 prettier: 3.3.3 prettier-linter-helpers: 1.0.0 synckit: 0.9.1 @@ -2444,23 +2460,23 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.13.0: + eslint@9.14.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.18.0 '@eslint/core': 0.7.0 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.13.0 + '@eslint/js': 9.14.0 '@eslint/plugin-kit': 0.2.2 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.1 '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.5 debug: 4.3.7 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 @@ -2736,7 +2752,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -2756,16 +2772,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.8.6): + jest-cli@29.7.0(@types/node@22.9.0): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.8.6) + create-jest: 29.7.0(@types/node@22.9.0) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.8.6) + jest-config: 29.7.0(@types/node@22.9.0) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2775,7 +2791,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.8.6): + jest-config@29.7.0(@types/node@22.9.0): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 @@ -2800,7 +2816,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.8.6 + '@types/node': 22.9.0 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2829,7 +2845,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2839,7 +2855,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.8.6 + '@types/node': 22.9.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2878,7 +2894,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2913,7 +2929,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2941,7 +2957,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -2987,7 +3003,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3006,7 +3022,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.8.6 + '@types/node': 22.9.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3015,17 +3031,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.8.6 + '@types/node': 22.9.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.8.6): + jest@29.7.0(@types/node@22.9.0): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.8.6) + jest-cli: 29.7.0(@types/node@22.9.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3061,7 +3077,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-private-community.9: {} + lemmy-js-client@0.20.0-instance-blocks.5: {} leven@3.1.0: {} @@ -3342,12 +3358,12 @@ snapshots: dependencies: typescript: 5.6.3 - ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.8.6))(typescript@5.6.3): + ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.8.6) + jest: 29.7.0(@types/node@22.9.0) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -3371,11 +3387,11 @@ snapshots: type-fest@0.21.3: {} - typescript-eslint@8.12.2(eslint@9.13.0)(typescript@5.6.3): + typescript-eslint@8.13.0(eslint@9.14.0)(typescript@5.6.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3) - '@typescript-eslint/parser': 8.12.2(eslint@9.13.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3) + '@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index 77b68e2fc..d75b711fc 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -25,16 +25,16 @@ import { getComments, createComment, getCommunityByName, - blockInstance, waitUntil, alphaUrl, delta, - betaAllowedInstances, searchPostLocal, longDelay, editCommunity, unfollows, + userBlockInstance, } from "./shared"; +import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; import { EditCommunity, EditSite } from "lemmy-js-client"; beforeAll(setupLogins); @@ -363,7 +363,7 @@ test("User blocks instance, communities are hidden", async () => { expect(listing_ids).toContain(postRes.post_view.post.ap_id); // block the beta instance - await blockInstance(alpha, alphaPost.community.instance_id, true); + await userBlockInstance(alpha, alphaPost.community.instance_id, true); // after blocking, post should not be in listing let listing2 = await getPosts(alpha, "All"); @@ -371,7 +371,7 @@ test("User blocks instance, communities are hidden", async () => { expect(listing_ids2.indexOf(postRes.post_view.post.ap_id)).toBe(-1); // unblock instance again - await blockInstance(alpha, alphaPost.community.instance_id, false); + await userBlockInstance(alpha, alphaPost.community.instance_id, false); // post should be included in listing let listing3 = await getPosts(alpha, "All"); @@ -455,9 +455,12 @@ test("Dont receive community activities after unsubscribe", async () => { expect(communityRes1.community_view.counts.subscribers).toBe(2); // temporarily block alpha, so that it doesn't know about unfollow - let editSiteForm: EditSite = {}; - editSiteForm.allowed_instances = ["lemmy-epsilon"]; - await beta.editSite(editSiteForm); + var allow_instance_params: AdminAllowInstanceParams = { + instance: "lemmy-alpha", + allow: false, + reason: undefined, + }; + await beta.adminAllowInstance(allow_instance_params); await longDelay(); // unfollow @@ -471,8 +474,8 @@ test("Dont receive community activities after unsubscribe", async () => { expect(communityRes2.community_view.counts.subscribers).toBe(2); // unblock alpha - editSiteForm.allowed_instances = betaAllowedInstances; - await beta.editSite(editSiteForm); + allow_instance_params.allow = true; + await beta.adminAllowInstance(allow_instance_params); await longDelay(); // create a post, it shouldnt reach beta diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index ed96451a2..7ac6e7221 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -41,6 +41,9 @@ afterAll(async () => { }); test("Upload image and delete it", async () => { + const healthz = await fetch(alphaUrl + "/pictrs/healthz"); + expect(healthz.status).toBe(200); + // Before running this test, you need to delete all previous images in the DB await deleteAllImages(alpha); diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 59e3d774e..a6063e0a2 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -40,6 +40,7 @@ import { createCommunity, } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; +import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; import { EditSite, ResolveObject } from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -87,12 +88,12 @@ async function assertPostFederation( } test("Create a post", async () => { - // Setup some allowlists and blocklists - const editSiteForm: EditSite = {}; - - editSiteForm.allowed_instances = []; - editSiteForm.blocked_instances = ["lemmy-alpha"]; - await epsilon.editSite(editSiteForm); + // Block alpha + var block_instance_params: AdminBlockInstanceParams = { + instance: "lemmy-alpha", + block: true, + }; + await epsilon.adminBlockInstance(block_instance_params); if (!betaCommunity) { throw "Missing beta community"; @@ -132,11 +133,9 @@ test("Create a post", async () => { resolvePost(epsilon, postRes.post_view.post), ).rejects.toStrictEqual(Error("not_found")); - // remove added allow/blocklists - editSiteForm.allowed_instances = []; - editSiteForm.blocked_instances = []; - await delta.editSite(editSiteForm); - await epsilon.editSite(editSiteForm); + // remove blocked instance + block_instance_params.block = false; + await epsilon.adminBlockInstance(block_instance_params); }); test("Create a post in a non-existent community", async () => { diff --git a/api_tests/src/private_community.spec.ts b/api_tests/src/private_community.spec.ts index 76faf800f..65340a1dd 100644 --- a/api_tests/src/private_community.spec.ts +++ b/api_tests/src/private_community.spec.ts @@ -1,6 +1,6 @@ jest.setTimeout(120000); -import { FollowCommunity } from "lemmy-js-client"; +import { FollowCommunity, LemmyHttp } from "lemmy-js-client"; import { alpha, setupLogins, @@ -21,6 +21,9 @@ import { resolveComment, likeComment, waitUntil, + gamma, + getPosts, + getComments, } from "./shared"; beforeAll(setupLogins); @@ -47,6 +50,7 @@ test("Follow a private community", async () => { await resolveCommunity(user, community.community_view.community.actor_id) ).community; expect(betaCommunity).toBeDefined(); + expect(betaCommunity?.community.visibility).toBe("Private"); const betaCommunityId = betaCommunity!.community.id; const follow_form: FollowCommunity = { community_id: betaCommunityId, @@ -148,16 +152,7 @@ test("Only followers can view and interact with private community content", asyn follow: true, }; await user.followCommunity(follow_form); - const pendingFollows1 = await waitUntil( - () => listCommunityPendingFollows(alpha), - f => f.items.length == 1, - ); - const approve = await approveCommunityPendingFollow( - alpha, - alphaCommunityId, - pendingFollows1.items[0].person.id, - ); - expect(approve.success).toBe(true); + approveFollower(alpha, alphaCommunityId); // now user can fetch posts and comments in community (using signed fetch), and create posts await waitUntil( @@ -212,3 +207,151 @@ test("Reject follower", async () => { c => c.community_view.subscribed == "NotSubscribed", ); }); + +test("Follow a private community and receive activities", async () => { + // create private community + const community = await createCommunity(alpha, randomString(10), "Private"); + expect(community.community_view.community.visibility).toBe("Private"); + const alphaCommunityId = community.community_view.community.id; + + // follow with users from beta and gamma + const betaCommunity = ( + await resolveCommunity(beta, community.community_view.community.actor_id) + ).community; + expect(betaCommunity).toBeDefined(); + const betaCommunityId = betaCommunity!.community.id; + const follow_form_beta: FollowCommunity = { + community_id: betaCommunityId, + follow: true, + }; + await beta.followCommunity(follow_form_beta); + await approveFollower(alpha, alphaCommunityId); + + const gammaCommunityId = ( + await resolveCommunity(gamma, community.community_view.community.actor_id) + ).community!.community.id; + const follow_form_gamma: FollowCommunity = { + community_id: gammaCommunityId, + follow: true, + }; + await gamma.followCommunity(follow_form_gamma); + await approveFollower(alpha, alphaCommunityId); + + // Follow is confirmed + await waitUntil( + () => getCommunity(beta, betaCommunityId), + c => c.community_view.subscribed == "Subscribed", + ); + await waitUntil( + () => getCommunity(gamma, gammaCommunityId), + c => c.community_view.subscribed == "Subscribed", + ); + + // create a post and comment from gamma + const post = await createPost(gamma, gammaCommunityId); + const post_id = post.post_view.post.id; + expect(post_id).toBeDefined(); + const comment = await createComment(gamma, post_id); + const comment_id = comment.comment_view.comment.id; + expect(comment_id).toBeDefined(); + + // post and comment were federated to beta + let posts = await waitUntil( + () => getPosts(beta, "All", betaCommunityId), + c => c.posts.length == 1, + ); + expect(posts.posts[0].post.ap_id).toBe(post.post_view.post.ap_id); + expect(posts.posts[0].post.name).toBe(post.post_view.post.name); + let comments = await waitUntil( + () => getComments(beta, posts.posts[0].post.id), + c => c.comments.length == 1, + ); + expect(comments.comments[0].comment.ap_id).toBe( + comment.comment_view.comment.ap_id, + ); + expect(comments.comments[0].comment.content).toBe( + comment.comment_view.comment.content, + ); +}); + +test("Fetch remote content in private community", async () => { + // create private community + const community = await createCommunity(alpha, randomString(10), "Private"); + expect(community.community_view.community.visibility).toBe("Private"); + const alphaCommunityId = community.community_view.community.id; + + const betaCommunityId = ( + await resolveCommunity(beta, community.community_view.community.actor_id) + ).community!.community.id; + const follow_form_beta: FollowCommunity = { + community_id: betaCommunityId, + follow: true, + }; + await beta.followCommunity(follow_form_beta); + await approveFollower(alpha, alphaCommunityId); + + // Follow is confirmed + await waitUntil( + () => getCommunity(beta, betaCommunityId), + c => c.community_view.subscribed == "Subscribed", + ); + + // beta creates post and comment + const post = await createPost(beta, betaCommunityId); + const post_id = post.post_view.post.id; + expect(post_id).toBeDefined(); + const comment = await createComment(beta, post_id); + const comment_id = comment.comment_view.comment.id; + expect(comment_id).toBeDefined(); + + // Wait for it to federate + await waitUntil( + () => resolveComment(alpha, comment.comment_view.comment), + p => p?.comment?.comment.id != undefined, + ); + + // create gamma user + const gammaCommunityId = ( + await resolveCommunity(gamma, community.community_view.community.actor_id) + ).community!.community.id; + const follow_form: FollowCommunity = { + community_id: gammaCommunityId, + follow: true, + }; + + // cannot fetch post yet + await expect(resolvePost(gamma, post.post_view.post)).rejects.toStrictEqual( + Error("not_found"), + ); + // follow community and approve + await gamma.followCommunity(follow_form); + await approveFollower(alpha, alphaCommunityId); + + // now user can fetch posts and comments in community (using signed fetch), and create posts. + // for this to work, beta checks with alpha if gamma is really an approved follower. + let resolvedPost = await waitUntil( + () => resolvePost(gamma, post.post_view.post), + p => p?.post?.post.id != undefined, + ); + expect(resolvedPost.post?.post.ap_id).toBe(post.post_view.post.ap_id); + const resolvedComment = await waitUntil( + () => resolveComment(gamma, comment.comment_view.comment), + p => p?.comment?.comment.id != undefined, + ); + expect(resolvedComment?.comment?.comment.ap_id).toBe( + comment.comment_view.comment.ap_id, + ); +}); + +async function approveFollower(user: LemmyHttp, community_id: number) { + let pendingFollows1 = await waitUntil( + () => listCommunityPendingFollows(user), + f => f.items.length == 1, + ); + const approve = await approveCommunityPendingFollow( + alpha, + community_id, + pendingFollows1.items[0].person.id, + ); + expect(approve.success).toBe(true); +} diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 95e916ef2..9b0662959 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -1,15 +1,13 @@ import { + AdminBlockInstanceParams, ApproveCommunityPendingFollower, BlockCommunity, BlockCommunityResponse, - BlockInstance, - BlockInstanceResponse, CommunityId, CommunityVisibility, CreatePrivateMessageReport, DeleteImage, EditCommunity, - GetCommunityPendingFollowsCount, GetCommunityPendingFollowsCountResponse, GetReplies, GetRepliesResponse, @@ -22,11 +20,13 @@ import { PostView, PrivateMessageReportResponse, SuccessResponse, + UserBlockInstanceParams, } from "lemmy-js-client"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { DeletePost } from "lemmy-js-client/dist/types/DeletePost"; import { EditPost } from "lemmy-js-client/dist/types/EditPost"; import { EditSite } from "lemmy-js-client/dist/types/EditSite"; +import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; import { FeaturePost } from "lemmy-js-client/dist/types/FeaturePost"; import { GetComments } from "lemmy-js-client/dist/types/GetComments"; import { GetCommentsResponse } from "lemmy-js-client/dist/types/GetCommentsResponse"; @@ -105,13 +105,6 @@ export const gamma = new LemmyHttp(gammaUrl, { fetchFunction }); export const delta = new LemmyHttp(deltaUrl, { fetchFunction }); export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); -export const betaAllowedInstances = [ - "lemmy-alpha", - "lemmy-gamma", - "lemmy-delta", - "lemmy-epsilon", -]; - const password = "lemmylemmy"; export async function setupLogins() { @@ -169,30 +162,29 @@ export async function setupLogins() { rate_limit_comment: 999, rate_limit_search: 999, }; - - // Set the blocks and auths for each - editSiteForm.allowed_instances = [ - "lemmy-beta", - "lemmy-gamma", - "lemmy-delta", - "lemmy-epsilon", - ]; await alpha.editSite(editSiteForm); - - editSiteForm.allowed_instances = betaAllowedInstances; await beta.editSite(editSiteForm); - - editSiteForm.allowed_instances = [ - "lemmy-alpha", - "lemmy-beta", - "lemmy-delta", - "lemmy-epsilon", - ]; await gamma.editSite(editSiteForm); - - // Setup delta allowed instance - editSiteForm.allowed_instances = ["lemmy-beta"]; await delta.editSite(editSiteForm); + await epsilon.editSite(editSiteForm); + + // Set the blocks for each + await allowInstance(alpha, "lemmy-beta"); + await allowInstance(alpha, "lemmy-gamma"); + await allowInstance(alpha, "lemmy-delta"); + await allowInstance(alpha, "lemmy-epsilon"); + + await allowInstance(beta, "lemmy-alpha"); + await allowInstance(beta, "lemmy-gamma"); + await allowInstance(beta, "lemmy-delta"); + await allowInstance(beta, "lemmy-epsilon"); + + await allowInstance(gamma, "lemmy-alpha"); + await allowInstance(gamma, "lemmy-beta"); + await allowInstance(gamma, "lemmy-delta"); + await allowInstance(gamma, "lemmy-epsilon"); + + await allowInstance(delta, "lemmy-beta"); // Create the main alpha/beta communities // Ignore thrown errors of duplicates @@ -209,6 +201,17 @@ export async function setupLogins() { } } +async function allowInstance(api: LemmyHttp, instance: string) { + const params: AdminAllowInstanceParams = { + instance, + allow: true, + }; + // Ignore errors from duplicate allows (because setup gets called for each test file) + try { + await api.adminAllowInstance(params); + } catch {} +} + export async function createPost( api: LemmyHttp, community_id: number, @@ -855,16 +858,16 @@ export function getPosts( return api.getPosts(form); } -export function blockInstance( +export function userBlockInstance( api: LemmyHttp, instance_id: InstanceId, block: boolean, -): Promise { - let form: BlockInstance = { +): Promise { + let form: UserBlockInstanceParams = { instance_id, block, }; - return api.blockInstance(form); + return api.userBlockInstance(form); } export function blockCommunity( @@ -988,7 +991,7 @@ export function getCommentParentId(comment: Comment): number | undefined { if (split.length > 1) { return Number(split[split.length - 2]); } else { - console.log(`Failed to extract comment parent id from ${comment.path}`); + console.error(`Failed to extract comment parent id from ${comment.path}`); return undefined; } } @@ -1006,7 +1009,7 @@ export async function waitUntil( result = await fetcher(); if (checker(result)) return result; } catch (error) { - //console.error(error); + console.error(error); } await delay( delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000, diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index 2edcf54ea..0cc747d2b 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -23,7 +23,12 @@ import { unfollows, saveUserSettingsBio, } from "./shared"; -import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client"; +import { + EditSite, + LemmyHttp, + SaveUserSettings, + UploadImage, +} from "lemmy-js-client"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; beforeAll(setupLogins); @@ -149,9 +154,14 @@ test("Create user with Arabic name", async () => { }); test("Create user with accept-language", async () => { + const edit: EditSite = { + discussion_languages: [32], + }; + await alpha.editSite(edit); + let lemmy_http = new LemmyHttp(alphaUrl, { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax - headers: { "Accept-Language": "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5" }, + headers: { "Accept-Language": "fr-CH, en;q=0.8, *;q=0.5" }, }); let user = await registerUser(lemmy_http, alphaUrl); diff --git a/config/defaults.hjson b/config/defaults.hjson index 96dc30b79..282b7957d 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -73,6 +73,15 @@ # # Requires pict-rs 0.5 "ProxyAllImages" + # Allows bypassing proxy for specific image hosts when using ProxyAllImages. + # + # imgur.com is bypassed by default to avoid rate limit errors. When specifying any bypass + # in the config, this default is ignored and you need to list imgur explicitly. To proxy imgur + # requests, specify a noop bypass list, eg `proxy_bypass_domains ["example.org"]`. + proxy_bypass_domains: [ + "i.imgur.com" + /* ... */ + ] # Timeout for uploading images to pictrs (in seconds) upload_timeout: 30 # Resize post thumbnails to this maximum width/height. @@ -122,5 +131,5 @@ } # Sets a response Access-Control-Allow-Origin CORS header # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - cors_origin: "*" + cors_origin: "lemmy.tld" } diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 87879f6cd..077426d32 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -34,7 +34,7 @@ tracing = { workspace = true } chrono = { workspace = true } url = { workspace = true } hound = "3.5.1" -sitemap-rs = "0.2.1" +sitemap-rs = "0.2.2" totp-rs = { version = "5.6.0", features = ["gen_secret", "otpauth"] } actix-web-httpauth = "0.8.2" diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 9e85788ea..4c5b4eae5 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -10,7 +10,7 @@ use lemmy_db_schema::{ source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, local_user::LocalUser, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, }, traits::{Crud, Joinable}, }; diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index a0e57061b..8689d2563 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ CommunityPersonBanForm, }, local_user::LocalUser, - moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, }, traits::{Bannable, Crud, Followable}, }; diff --git a/crates/api/src/community/hide.rs b/crates/api/src/community/hide.rs index 077ed1c5e..f494ad732 100644 --- a/crates/api/src/community/hide.rs +++ b/crates/api/src/community/hide.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityUpdateForm}, - moderator::{ModHideCommunity, ModHideCommunityForm}, + mod_log::moderator::{ModHideCommunity, ModHideCommunityForm}, }, traits::Crud, }; diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index a5255e5e1..e60b50aa2 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -8,7 +8,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModTransferCommunity, ModTransferCommunityForm}, + mod_log::moderator::{ModTransferCommunity, ModTransferCommunityForm}, }, traits::{Crud, Joinable}, }; diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 3ab2ba277..6a2c94332 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -19,7 +19,7 @@ use lemmy_db_schema::{ CommunityPersonBanForm, }, local_site::LocalSite, - moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, person::Person, }, traits::{Bannable, Crud, Followable}, @@ -145,7 +145,7 @@ fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> LemmyResult) -> LemmyResult, context: Data, local_user_view: LocalUserView, -) -> LemmyResult> { - let post_ids = HashSet::from_iter(data.post_ids.clone()); - - if post_ids.len() > MAX_API_PARAM_ELEMENTS { - Err(LemmyErrorType::TooManyItems)?; - } - +) -> LemmyResult> { let person_id = local_user_view.person.id; + let post_id = data.post_id; // Mark the post as hidden / unhidden if data.hide { - PostHide::hide(&mut context.pool(), post_ids, person_id) + PostHide::hide(&mut context.pool(), post_id, person_id) .await .with_lemmy_type(LemmyErrorType::CouldntHidePost)?; } else { - PostHide::unhide(&mut context.pool(), post_ids, person_id) + PostHide::unhide(&mut context.pool(), post_id, person_id) .await .with_lemmy_type(LemmyErrorType::CouldntHidePost)?; } - Ok(Json(SuccessResponse::default())) + let post_view = PostView::read( + &mut context.pool(), + post_id, + Some(&local_user_view.local_user), + false, + ) + .await?; + + Ok(Json(PostResponse { post_view })) } diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index ec01e3e8c..031e3f0db 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -5,18 +5,12 @@ use lemmy_api_common::{ context::LemmyContext, post::{CreatePostLike, PostResponse}, send_activity::{ActivityChannel, SendActivityData}, - utils::{ - check_bot_account, - check_community_user_action, - check_local_vote_mode, - mark_post_as_read, - VoteItem, - }, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem}, }; use lemmy_db_schema::{ source::{ local_site::LocalSite, - post::{PostLike, PostLikeForm}, + post::{PostLike, PostLikeForm, PostRead, PostReadForm}, }, traits::Likeable, }; @@ -53,11 +47,7 @@ pub async fn like_post( ) .await?; - let like_form = PostLikeForm { - post_id: data.post_id, - person_id: local_user_view.person.id, - score: data.score, - }; + let like_form = PostLikeForm::new(data.post_id, local_user_view.person.id, data.score); // Remove any likes first let person_id = local_user_view.person.id; @@ -72,7 +62,9 @@ pub async fn like_post( .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; } - mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + // Mark Post Read + let read_form = PostReadForm::new(post_id, person_id); + PostRead::mark_as_read(&mut context.pool(), &read_form).await?; ActivityChannel::submit_activity( SendActivityData::LikePostOrComment { diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index 011770c2e..ad7fa7264 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -9,7 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - moderator::{ModLockPost, ModLockPostForm}, + mod_log::moderator::{ModLockPost, ModLockPostForm}, post::{Post, PostUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/post/mark_many_read.rs b/crates/api/src/post/mark_many_read.rs new file mode 100644 index 000000000..82c2c0b06 --- /dev/null +++ b/crates/api/src/post/mark_many_read.rs @@ -0,0 +1,24 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{context::LemmyContext, post::MarkManyPostsAsRead, SuccessResponse}; +use lemmy_db_schema::source::post::PostRead; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::{LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS}; + +#[tracing::instrument(skip(context))] +pub async fn mark_posts_as_read( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let post_ids = &data.post_ids; + if post_ids.len() > MAX_API_PARAM_ELEMENTS { + Err(LemmyErrorType::TooManyItems)?; + } + + let person_id = local_user_view.person.id; + + // Mark the posts as read + PostRead::mark_many_as_read(&mut context.pool(), post_ids, person_id).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/post/mark_read.rs b/crates/api/src/post/mark_read.rs index 3e534675a..2d3284375 100644 --- a/crates/api/src/post/mark_read.rs +++ b/crates/api/src/post/mark_read.rs @@ -1,34 +1,35 @@ use actix_web::web::{Data, Json}; -use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse}; -use lemmy_db_schema::source::post::PostRead; -use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS}; -use std::collections::HashSet; +use lemmy_api_common::{ + context::LemmyContext, + post::{MarkPostAsRead, PostResponse}, +}; +use lemmy_db_schema::source::post::{PostRead, PostReadForm}; +use lemmy_db_views::structs::{LocalUserView, PostView}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn mark_post_as_read( data: Json, context: Data, local_user_view: LocalUserView, -) -> LemmyResult> { - let post_ids = HashSet::from_iter(data.post_ids.clone()); - - if post_ids.len() > MAX_API_PARAM_ELEMENTS { - Err(LemmyErrorType::TooManyItems)?; - } - +) -> LemmyResult> { let person_id = local_user_view.person.id; + let post_id = data.post_id; // Mark the post as read / unread + let form = PostReadForm::new(post_id, person_id); if data.read { - PostRead::mark_as_read(&mut context.pool(), post_ids, person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?; + PostRead::mark_as_read(&mut context.pool(), &form).await?; } else { - PostRead::mark_as_unread(&mut context.pool(), post_ids, person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?; + PostRead::mark_as_unread(&mut context.pool(), &form).await?; } + let post_view = PostView::read( + &mut context.pool(), + post_id, + Some(&local_user_view.local_user), + false, + ) + .await?; - Ok(Json(SuccessResponse::default())) + Ok(Json(PostResponse { post_view })) } diff --git a/crates/api/src/post/mod.rs b/crates/api/src/post/mod.rs index 7287010f7..97410f097 100644 --- a/crates/api/src/post/mod.rs +++ b/crates/api/src/post/mod.rs @@ -4,5 +4,6 @@ pub mod hide; pub mod like; pub mod list_post_likes; pub mod lock; +pub mod mark_many_read; pub mod mark_read; pub mod save; diff --git a/crates/api/src/post/save.rs b/crates/api/src/post/save.rs index 4549b62b1..cebbd7fd5 100644 --- a/crates/api/src/post/save.rs +++ b/crates/api/src/post/save.rs @@ -2,10 +2,9 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, post::{PostResponse, SavePost}, - utils::mark_post_as_read, }; use lemmy_db_schema::{ - source::post::{PostSaved, PostSavedForm}, + source::post::{PostRead, PostReadForm, PostSaved, PostSavedForm}, traits::Saveable, }; use lemmy_db_views::structs::{LocalUserView, PostView}; @@ -17,10 +16,7 @@ pub async fn save_post( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let post_saved_form = PostSavedForm { - post_id: data.post_id, - person_id: local_user_view.person.id, - }; + let post_saved_form = PostSavedForm::new(data.post_id, local_user_view.person.id); if data.save { PostSaved::save(&mut context.pool(), &post_saved_form) @@ -42,7 +38,8 @@ pub async fn save_post( ) .await?; - mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + let read_form = PostReadForm::new(post_id, person_id); + PostRead::mark_as_read(&mut context.pool(), &read_form).await?; Ok(Json(PostResponse { post_view })) } diff --git a/crates/api/src/site/admin_allow_instance.rs b/crates/api/src/site/admin_allow_instance.rs new file mode 100644 index 000000000..81879ecae --- /dev/null +++ b/crates/api/src/site/admin_allow_instance.rs @@ -0,0 +1,53 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + site::AdminAllowInstanceParams, + utils::is_admin, + LemmyErrorType, + SuccessResponse, +}; +use lemmy_db_schema::source::{ + federation_allowlist::{FederationAllowList, FederationAllowListForm}, + instance::Instance, + mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn admin_allow_instance( + data: Json, + local_user_view: LocalUserView, + context: Data, +) -> LemmyResult> { + is_admin(&local_user_view)?; + + let blocklist = Instance::blocklist(&mut context.pool()).await?; + if !blocklist.is_empty() { + Err(LemmyErrorType::CannotCombineFederationBlocklistAndAllowlist)?; + } + + let instance_id = Instance::read_or_create(&mut context.pool(), data.instance.clone()) + .await? + .id; + let form = FederationAllowListForm { + instance_id, + updated: None, + }; + if data.allow { + FederationAllowList::allow(&mut context.pool(), &form).await?; + } else { + FederationAllowList::unallow(&mut context.pool(), instance_id).await?; + } + + let mod_log_form = AdminAllowInstanceForm { + instance_id, + admin_person_id: local_user_view.person.id, + reason: data.reason.clone(), + allowed: data.allow, + }; + AdminAllowInstance::insert(&mut context.pool(), &mod_log_form).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/site/admin_block_instance.rs b/crates/api/src/site/admin_block_instance.rs new file mode 100644 index 000000000..54962ccf3 --- /dev/null +++ b/crates/api/src/site/admin_block_instance.rs @@ -0,0 +1,56 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + site::AdminBlockInstanceParams, + utils::is_admin, + LemmyErrorType, + SuccessResponse, +}; +use lemmy_db_schema::source::{ + federation_blocklist::{FederationBlockList, FederationBlockListForm}, + instance::Instance, + mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn admin_block_instance( + data: Json, + local_user_view: LocalUserView, + context: Data, +) -> LemmyResult> { + is_admin(&local_user_view)?; + + let allowlist = Instance::allowlist(&mut context.pool()).await?; + if !allowlist.is_empty() { + Err(LemmyErrorType::CannotCombineFederationBlocklistAndAllowlist)?; + } + + let instance_id = Instance::read_or_create(&mut context.pool(), data.instance.clone()) + .await? + .id; + let form = FederationBlockListForm { + instance_id, + expires: data.expires, + updated: None, + }; + + if data.block { + FederationBlockList::block(&mut context.pool(), &form).await?; + } else { + FederationBlockList::unblock(&mut context.pool(), instance_id).await?; + } + + let mod_log_form = AdminBlockInstanceForm { + instance_id, + admin_person_id: local_user_view.person.id, + blocked: data.block, + reason: data.reason.clone(), + when_: data.expires, + }; + AdminBlockInstance::insert(&mut context.pool(), &mod_log_form).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 97ad7e2e5..86b80be3e 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -6,7 +6,7 @@ use lemmy_db_schema::{ language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, local_user::{LocalUser, LocalUserUpdateForm}, - moderator::{ModAdd, ModAddForm}, + mod_log::moderator::{ModAdd, ModAddForm}, oauth_provider::OAuthProvider, tagline::Tagline, }, diff --git a/crates/api/src/site/mod.rs b/crates/api/src/site/mod.rs index f18dea3d0..52e882bb3 100644 --- a/crates/api/src/site/mod.rs +++ b/crates/api/src/site/mod.rs @@ -1,7 +1,9 @@ -pub mod block; +pub mod admin_allow_instance; +pub mod admin_block_instance; pub mod federated_instances; pub mod leave_admin; pub mod list_all_media; pub mod mod_log; pub mod purge; pub mod registration_applications; +pub mod user_block_instance; diff --git a/crates/api/src/site/mod_log.rs b/crates/api/src/site/mod_log.rs index 8f5538566..bbf147666 100644 --- a/crates/api/src/site/mod_log.rs +++ b/crates/api/src/site/mod_log.rs @@ -7,6 +7,8 @@ use lemmy_api_common::{ use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType}; use lemmy_db_views::structs::LocalUserView; use lemmy_db_views_moderator::structs::{ + AdminAllowInstanceView, + AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, @@ -121,6 +123,8 @@ pub async fn get_mod_log( admin_purged_communities, admin_purged_posts, admin_purged_comments, + admin_block_instance, + admin_allow_instance, ) = if data.community_id.is_none() { ( match type_ { @@ -161,6 +165,18 @@ pub async fn get_mod_log( } _ => Default::default(), }, + match type_ { + All | AdminBlockInstance if other_person_id.is_none() => { + AdminBlockInstanceView::list(&mut context.pool(), params).await? + } + _ => Default::default(), + }, + match type_ { + All | AdminAllowInstance if other_person_id.is_none() => { + AdminAllowInstanceView::list(&mut context.pool(), params).await? + } + _ => Default::default(), + }, ) } else { Default::default() @@ -183,5 +199,7 @@ pub async fn get_mod_log( admin_purged_posts, admin_purged_comments, hidden_communities, + admin_block_instance, + admin_allow_instance, })) } diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index ae79a835a..5208cc397 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ source::{ comment::Comment, local_user::LocalUser, - moderator::{AdminPurgeComment, AdminPurgeCommentForm}, + mod_log::admin::{AdminPurgeComment, AdminPurgeCommentForm}, }, traits::Crud, }; diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index f0252e303..c55f753dc 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ source::{ community::Community, local_user::LocalUser, - moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm}, + mod_log::admin::{AdminPurgeCommunity, AdminPurgeCommunityForm}, }, traits::Crud, }; diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 6dad4ce65..0f15e7726 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -11,7 +11,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_user::LocalUser, - moderator::{AdminPurgePerson, AdminPurgePersonForm}, + mod_log::admin::{AdminPurgePerson, AdminPurgePersonForm}, person::{Person, PersonUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index f808269e7..e726945f5 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -11,7 +11,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_user::LocalUser, - moderator::{AdminPurgePost, AdminPurgePostForm}, + mod_log::admin::{AdminPurgePost, AdminPurgePostForm}, post::Post, }, traits::Crud, diff --git a/crates/api/src/site/block.rs b/crates/api/src/site/user_block_instance.rs similarity index 79% rename from crates/api/src/site/block.rs rename to crates/api/src/site/user_block_instance.rs index 823dda612..940538833 100644 --- a/crates/api/src/site/block.rs +++ b/crates/api/src/site/user_block_instance.rs @@ -1,9 +1,6 @@ use activitypub_federation::config::Data; use actix_web::web::Json; -use lemmy_api_common::{ - context::LemmyContext, - site::{BlockInstance, BlockInstanceResponse}, -}; +use lemmy_api_common::{context::LemmyContext, site::UserBlockInstanceParams, SuccessResponse}; use lemmy_db_schema::{ source::instance_block::{InstanceBlock, InstanceBlockForm}, traits::Blockable, @@ -12,11 +9,11 @@ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] -pub async fn block_instance( - data: Json, +pub async fn user_block_instance( + data: Json, local_user_view: LocalUserView, context: Data, -) -> LemmyResult> { +) -> LemmyResult> { let instance_id = data.instance_id; let person_id = local_user_view.person.id; if local_user_view.person.instance_id == instance_id { @@ -38,7 +35,5 @@ pub async fn block_instance( .with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?; } - Ok(Json(BlockInstanceResponse { - blocked: data.block, - })) + Ok(Json(SuccessResponse::default())) } diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index f939985e8..3ae14717d 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -36,6 +36,7 @@ full = [ "futures", "jsonwebtoken", "mime", + "moka", ] [dependencies] @@ -58,22 +59,18 @@ uuid = { workspace = true, optional = true } tokio = { workspace = true, optional = true } reqwest = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true } -moka.workspace = true +moka = { workspace = true, optional = true } anyhow.workspace = true actix-web = { workspace = true, optional = true } enum-map = { workspace = true } urlencoding = { workspace = true } mime = { version = "0.3.17", optional = true } +mime_guess = "2.0.5" webpage = { version = "2.0", default-features = false, features = [ "serde", ], optional = true } -encoding_rs = { version = "0.8.34", optional = true } +encoding_rs = { version = "0.8.35", optional = true } jsonwebtoken = { version = "9.3.0", optional = true } -# necessary for wasmt compilation -getrandom = { version = "0.2.15", features = ["js"] } - -[package.metadata.cargo-shear] -ignored = ["getrandom"] [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs index d40f4c23d..b73c0e482 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -17,8 +17,10 @@ use lemmy_db_schema::{ actor_language::CommunityLanguage, comment::Comment, comment_reply::{CommentReply, CommentReplyInsertForm}, + community::Community, person::Person, person_mention::{PersonMention, PersonMentionInsertForm}, + post::Post, }, traits::Crud, }; @@ -101,17 +103,28 @@ pub async fn send_local_notifs( let mut recipient_ids = Vec::new(); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); - // let person = my_local_user.person; - // Read the comment view to get extra info - let comment_view = CommentView::read( - &mut context.pool(), - comment_id, - local_user_view.map(|view| &view.local_user), - ) - .await?; - let comment = comment_view.comment; - let post = comment_view.post; - let community = comment_view.community; + // When called from api code, we have local user view and can read with CommentView + // to reduce db queries. But when receiving a federated comment the user view is None, + // which means that comments inside private communities cant be read. As a workaround + // we need to read the items manually to bypass this check. + let (comment, post, community) = if let Some(local_user_view) = local_user_view { + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await?; + ( + comment_view.comment, + comment_view.post, + comment_view.community, + ) + } else { + let comment = Comment::read(&mut context.pool(), comment_id).await?; + let post = Post::read(&mut context.pool(), comment.post_id).await?; + let community = Community::read(&mut context.pool(), post.community_id).await?; + (comment, post, community) + }; // Send the local mentions for mention in mentions diff --git a/crates/api_common/src/context.rs b/crates/api_common/src/context.rs index c6ab23bfc..b578914d1 100644 --- a/crates/api_common/src/context.rs +++ b/crates/api_common/src/context.rs @@ -55,6 +55,7 @@ impl LemmyContext { /// Initialize a context for use in tests which blocks federation network calls. /// /// Do not use this in production code. + #[allow(clippy::expect_used)] pub async fn init_test_federation_config() -> FederationConfig { // call this to run migrations let pool = build_db_pool_for_tests(); diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 742dc88db..b95cf5e77 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -178,6 +178,9 @@ pub struct SaveUserSettings { pub show_downvotes: Option, #[cfg_attr(feature = "full", ts(optional))] pub show_upvote_percentage: Option, + /// Whether to automatically mark fetched posts as read. + #[cfg_attr(feature = "full", ts(optional))] + pub auto_mark_fetched_posts_as_read: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 87349bd6b..6442a1599 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -109,6 +109,9 @@ pub struct GetPosts { /// If true, then show the nsfw posts (even if your user setting is to hide them) #[cfg_attr(feature = "full", ts(optional))] pub show_nsfw: Option, + /// Whether to automatically mark fetched posts as read. + #[cfg_attr(feature = "full", ts(optional))] + pub mark_as_read: Option, #[cfg_attr(feature = "full", ts(optional))] /// If true, then only show posts with no comments pub no_comments_only: Option, @@ -195,17 +198,26 @@ pub struct RemovePost { #[cfg_attr(feature = "full", ts(export))] /// Mark a post as read. pub struct MarkPostAsRead { - pub post_ids: Vec, + pub post_id: PostId, pub read: bool, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Mark several posts as read. +pub struct MarkManyPostsAsRead { + pub post_ids: Vec, +} + #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Hide a post from list views pub struct HidePost { - pub post_ids: Vec, + pub post_id: PostId, pub hide: bool, } diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index 96d64d0e5..cc506b896 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -18,12 +18,11 @@ use lemmy_db_schema::{ }, }; use lemmy_utils::{ - error::{LemmyError, LemmyErrorType, LemmyResult}, + error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, settings::structs::{PictrsImageMode, Settings}, REQWEST_TIMEOUT, VERSION, }; -use mime::Mime; use reqwest::{ header::{CONTENT_TYPE, RANGE}, Client, @@ -61,13 +60,23 @@ pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResu // server may ignore this and still respond with the full response .header(RANGE, format!("bytes=0-{}", bytes_to_fetch - 1)) /* -1 because inclusive */ .send() - .await?; + .await? + .error_for_status()?; - let content_type: Option = response - .headers() - .get(CONTENT_TYPE) - .and_then(|h| h.to_str().ok()) - .and_then(|h| h.parse().ok()); + // In some cases servers send a wrong mime type for images, which prevents thumbnail + // generation. To avoid this we also try to guess the mime type from file extension. + let content_type = mime_guess::from_path(url.path()) + .first() + // If you can guess that its an image type, then return that first. + .filter(|guess| guess.type_() == mime::IMAGE) + // Otherwise, get the content type from the headers + .or( + response + .headers() + .get(CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.parse().ok()), + ); let opengraph_data = { // if the content type is not text/html, we don't need to parse it @@ -308,7 +317,8 @@ pub async fn purge_image_from_pictrs(image_url: &Url, context: &LemmyContext) -> .timeout(REQWEST_TIMEOUT) .header("x-api-token", pictrs_api_key) .send() - .await?; + .await? + .error_for_status()?; let response: PictrsPurgeResponse = response.json().await.map_err(LemmyError::from)?; @@ -333,8 +343,8 @@ pub async fn delete_image_from_pictrs( .delete(&url) .timeout(REQWEST_TIMEOUT) .send() - .await - .map_err(LemmyError::from)?; + .await? + .error_for_status()?; Ok(()) } @@ -366,6 +376,7 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L .timeout(REQWEST_TIMEOUT) .send() .await? + .error_for_status()? .json::() .await?; @@ -406,16 +417,14 @@ pub async fn fetch_pictrs_proxied_image_details( // Pictrs needs you to fetch the proxied image before you can fetch the details let proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}"); - let res = context + context .client() .get(&proxy_url) .timeout(REQWEST_TIMEOUT) .send() .await? - .status(); - if !res.is_success() { - Err(LemmyErrorType::NotAnImageType)? - } + .error_for_status() + .with_lemmy_type(LemmyErrorType::NotAnImageType)?; let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}"); @@ -425,6 +434,7 @@ pub async fn fetch_pictrs_proxied_image_details( .timeout(REQWEST_TIMEOUT) .send() .await? + .error_for_status()? .json() .await?; @@ -521,7 +531,7 @@ mod tests { // root relative url let html_bytes = b""; - let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); + let metadata = extract_opengraph_data(html_bytes, &url)?; assert_eq!( metadata.image, Some(Url::parse("https://example.com/image.jpg")?.into()) @@ -529,7 +539,7 @@ mod tests { // base relative url let html_bytes = b""; - let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); + let metadata = extract_opengraph_data(html_bytes, &url)?; assert_eq!( metadata.image, Some(Url::parse("https://example.com/one/image.jpg")?.into()) @@ -537,7 +547,7 @@ mod tests { // absolute url let html_bytes = b""; - let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); + let metadata = extract_opengraph_data(html_bytes, &url)?; assert_eq!( metadata.image, Some(Url::parse("https://cdn.host.com/image.jpg")?.into()) @@ -545,7 +555,7 @@ mod tests { // protocol relative url let html_bytes = b""; - let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); + let metadata = extract_opengraph_data(html_bytes, &url)?; assert_eq!( metadata.image, Some(Url::parse("https://example.com/image.jpg")?.into()) diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 40a5cc42d..9babe423c 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -43,6 +43,8 @@ use lemmy_db_views_actor::structs::{ PersonView, }; use lemmy_db_views_moderator::structs::{ + AdminAllowInstanceView, + AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, @@ -183,6 +185,8 @@ pub struct GetModlogResponse { pub admin_purged_posts: Vec, pub admin_purged_comments: Vec, pub hidden_communities: Vec, + pub admin_block_instance: Vec, + pub admin_allow_instance: Vec, } #[skip_serializing_none] @@ -265,10 +269,6 @@ pub struct CreateSite { #[cfg_attr(feature = "full", ts(optional))] pub captcha_difficulty: Option, #[cfg_attr(feature = "full", ts(optional))] - pub allowed_instances: Option>, - #[cfg_attr(feature = "full", ts(optional))] - pub blocked_instances: Option>, - #[cfg_attr(feature = "full", ts(optional))] pub registration_mode: Option, #[cfg_attr(feature = "full", ts(optional))] pub oauth_registration: Option, @@ -394,12 +394,6 @@ pub struct EditSite { /// The captcha difficulty. Can be easy, medium, or hard #[cfg_attr(feature = "full", ts(optional))] pub captcha_difficulty: Option, - /// A list of allowed instances. If none are set, federation is open. - #[cfg_attr(feature = "full", ts(optional))] - pub allowed_instances: Option>, - /// A list of blocked instances. - #[cfg_attr(feature = "full", ts(optional))] - pub blocked_instances: Option>, /// A list of blocked URLs #[cfg_attr(feature = "full", ts(optional))] pub blocked_urls: Option>, @@ -514,6 +508,7 @@ pub struct ReadableFederationState { next_retry: Option>, } +#[allow(clippy::expect_used)] impl From for ReadableFederationState { fn from(internal_state: FederationQueueState) -> Self { ReadableFederationState { @@ -647,15 +642,29 @@ pub struct GetUnreadRegistrationApplicationCountResponse { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Block an instance as user -pub struct BlockInstance { +pub struct UserBlockInstanceParams { pub instance_id: InstanceId, pub block: bool, } -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -pub struct BlockInstanceResponse { - pub blocked: bool, +pub struct AdminBlockInstanceParams { + pub instance: String, + pub block: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminAllowInstanceParams { + pub instance: String, + pub allow: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 64b0df346..3b8a00197 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -23,12 +23,17 @@ use lemmy_db_schema::{ local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_site_url_blocklist::LocalSiteUrlBlocklist, - moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm}, + mod_log::moderator::{ + ModRemoveComment, + ModRemoveCommentForm, + ModRemovePost, + ModRemovePostForm, + }, oauth_account::OAuthAccount, password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, person_block::PersonBlock, - post::{Post, PostLike, PostRead}, + post::{Post, PostLike}, registration_application::RegistrationApplication, site::Site, }, @@ -60,12 +65,13 @@ use lemmy_utils::{ slurs::{build_slur_regex, remove_slurs}, validation::clean_urls_in_text, }, + CacheLock, CACHE_DURATION_FEDERATION, }; use moka::future::Cache; use regex::{escape, Regex, RegexSet}; use rosetta_i18n::{Language, LanguageId}; -use std::{collections::HashSet, sync::LazyLock}; +use std::sync::LazyLock; use tracing::warn; use url::{ParseError, Url}; use urlencoding::encode; @@ -141,19 +147,6 @@ pub fn is_top_mod( } } -/// Marks a post as read for a given person. -#[tracing::instrument(skip_all)] -pub async fn mark_post_as_read( - person_id: PersonId, - post_id: PostId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - PostRead::mark_as_read(pool, HashSet::from([post_id]), person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?; - 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( @@ -166,7 +159,6 @@ pub async fn update_read_comments( person_id, post_id, read_comments, - ..PersonPostAggregatesForm::default() }; PersonPostAggregates::upsert(pool, &person_post_agg_form).await?; @@ -456,7 +448,11 @@ pub async fn send_password_reset_email( // Generate a random token let token = uuid::Uuid::new_v4().to_string(); - let email = &user.local_user.email.clone().expect("email"); + let email = &user + .local_user + .email + .clone() + .ok_or(LemmyErrorType::EmailRequired)?; let lang = get_interface_language(user); let subject = &lang.password_reset_subject(&user.person.name); let protocol_and_hostname = settings.get_protocol_and_hostname(); @@ -506,6 +502,7 @@ pub fn get_interface_language_from_settings(user: &LocalUserView) -> Lang { lang_str_to_lang(&user.local_user.interface_language) } +#[allow(clippy::expect_used)] fn lang_str_to_lang(lang: &str) -> Lang { let lang_id = LanguageId::new(lang); Lang::from_language_id(&lang_id).unwrap_or_else(|| { @@ -532,11 +529,11 @@ pub fn local_site_rate_limit_to_rate_limit_config( }) } -pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option { +pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option> { build_slur_regex(local_site.slur_filter_regex.as_deref()) } -pub fn local_site_opt_to_slur_regex(local_site: &Option) -> Option { +pub fn local_site_opt_to_slur_regex(local_site: &Option) -> Option> { local_site .as_ref() .map(local_site_to_slur_regex) @@ -544,7 +541,7 @@ pub fn local_site_opt_to_slur_regex(local_site: &Option) -> Option LemmyResult { - static URL_BLOCKLIST: LazyLock> = LazyLock::new(|| { + static URL_BLOCKLIST: CacheLock = LazyLock::new(|| { Cache::builder() .max_capacity(1) .time_to_live(CACHE_DURATION_FEDERATION) @@ -571,7 +568,11 @@ pub async fn send_application_approved_email( user: &LocalUserView, settings: &Settings, ) -> LemmyResult<()> { - let email = &user.local_user.email.clone().expect("email"); + let email = &user + .local_user + .email + .clone() + .ok_or(LemmyErrorType::EmailRequired)?; let lang = get_interface_language(user); let subject = lang.registration_approved_subject(&user.person.actor_id); let body = lang.registration_approved_body(&settings.hostname); @@ -593,7 +594,11 @@ pub async fn send_new_applicant_email_to_admins( ); for admin in &admins { - let email = &admin.local_user.email.clone().expect("email"); + let email = &admin + .local_user + .email + .clone() + .ok_or(LemmyErrorType::EmailRequired)?; let lang = get_interface_language_from_settings(admin); let subject = lang.new_application_subject(&settings.hostname, applicant_username); let body = lang.new_application_body(applications_link); @@ -615,11 +620,13 @@ pub async fn send_new_report_email_to_admins( let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),); for admin in &admins { - let email = &admin.local_user.email.clone().expect("email"); - let lang = get_interface_language_from_settings(admin); - let subject = lang.new_report_subject(&settings.hostname, reported_username, reporter_username); - let body = lang.new_report_body(reports_link); - send_email(&subject, email, &admin.person.name, &body, settings).await?; + if let Some(email) = &admin.local_user.email { + let lang = get_interface_language_from_settings(admin); + let subject = + lang.new_report_subject(&settings.hostname, reported_username, reporter_username); + let body = lang.new_report_body(reports_link); + send_email(&subject, email, &admin.person.name, &body, settings).await?; + } } Ok(()) } @@ -1044,7 +1051,7 @@ pub fn check_conflicting_like_filters( pub async fn process_markdown( text: &str, - slur_regex: &Option, + slur_regex: &Option>, url_blocklist: &RegexSet, context: &LemmyContext, ) -> LemmyResult { @@ -1076,7 +1083,7 @@ pub async fn process_markdown( pub async fn process_markdown_opt( text: &Option, - slur_regex: &Option, + slur_regex: &Option>, url_blocklist: &RegexSet, context: &LemmyContext, ) -> LemmyResult> { diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index 723864705..3f1a00ccd 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -25,7 +25,6 @@ tracing = { workspace = true } url = { workspace = true } futures.workspace = true uuid = { workspace = true } -moka.workspace = true anyhow.workspace = true chrono.workspace = true webmention = "0.6.0" diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 65aa1f612..edcf7db30 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -16,9 +16,8 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ - impls::actor_language::default_post_language, + impls::actor_language::validate_post_language, source::{ - actor_language::CommunityLanguage, comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, comment_reply::{CommentReply, CommentReplyUpdateForm}, local_site::LocalSite, @@ -93,21 +92,13 @@ pub async fn create_comment( check_comment_depth(parent)?; } - // attempt to set default language if none was provided - let language_id = match data.language_id { - Some(lid) => lid, - None => { - default_post_language( - &mut context.pool(), - community_id, - local_user_view.local_user.id, - ) - .await? - } - }; - - CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id) - .await?; + let language_id = validate_post_language( + &mut context.pool(), + data.language_id, + community_id, + local_user_view.local_user.id, + ) + .await?; let comment_form = CommentInsertForm { language_id: Some(language_id), diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 4e8a1871a..1ac6201e8 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -12,7 +12,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, local_user::LocalUser, - moderator::{ModRemoveComment, ModRemoveCommentForm}, + mod_log::moderator::{ModRemoveComment, ModRemoveCommentForm}, }, traits::{Crud, Reportable}, }; diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 95cc85fe4..1af026204 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -1,5 +1,6 @@ use activitypub_federation::config::Data; use actix_web::web::Json; +use chrono::Utc; use lemmy_api_common::{ build_response::{build_comment_response, send_local_notifs}, comment::{CommentResponse, EditComment}, @@ -13,13 +14,12 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ + impls::actor_language::validate_post_language, source::{ - actor_language::CommunityLanguage, comment::{Comment, CommentUpdateForm}, local_site::LocalSite, }, traits::Crud, - utils::naive_now, }; use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_utils::{ @@ -55,14 +55,13 @@ pub async fn update_comment( Err(LemmyErrorType::NoCommentEditAllowed)? } - if let Some(language_id) = data.language_id { - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - language_id, - orig_comment.community.id, - ) - .await?; - } + let language_id = validate_post_language( + &mut context.pool(), + data.language_id, + orig_comment.community.id, + local_user_view.local_user.id, + ) + .await?; let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; @@ -74,8 +73,8 @@ pub async fn update_comment( let comment_id = data.comment_id; let form = CommentUpdateForm { content, - language_id: data.language_id, - updated: Some(Some(naive_now())), + language_id: Some(language_id), + updated: Some(Some(Utc::now())), ..Default::default() }; let updated_comment = Comment::update(&mut context.pool(), comment_id, &form) diff --git a/crates/api_crud/src/community/remove.rs b/crates/api_crud/src/community/remove.rs index c506bde1b..7dc78a37a 100644 --- a/crates/api_crud/src/community/remove.rs +++ b/crates/api_crud/src/community/remove.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityUpdateForm}, - moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, + mod_log::moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, }, traits::Crud, }; diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index 3dca7d892..d9c062c53 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -1,6 +1,7 @@ use super::check_community_visibility_allowed; use activitypub_federation::config::Data; use actix_web::web::Json; +use chrono::Utc; use lemmy_api_common::{ build_response::build_community_response, community::{CommunityResponse, EditCommunity}, @@ -22,7 +23,7 @@ use lemmy_db_schema::{ local_site::LocalSite, }, traits::Crud, - utils::{diesel_string_update, diesel_url_update, naive_now}, + utils::{diesel_string_update, diesel_url_update}, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ @@ -95,7 +96,7 @@ pub async fn update_community( nsfw: data.nsfw, posting_restricted_to_mods: data.posting_restricted_to_mods, visibility: data.visibility, - updated: Some(Some(naive_now())), + updated: Some(Some(Utc::now())), ..Default::default() }; diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs index b4734bf36..29ba19b49 100644 --- a/crates/api_crud/src/oauth_provider/update.rs +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -1,10 +1,11 @@ use activitypub_federation::config::Data; use actix_web::web::Json; +use chrono::Utc; use lemmy_api_common::{context::LemmyContext, oauth_provider::EditOAuthProvider, utils::is_admin}; use lemmy_db_schema::{ source::oauth_provider::{OAuthProvider, OAuthProviderUpdateForm}, traits::Crud, - utils::{diesel_required_string_update, diesel_required_url_update, naive_now}, + utils::{diesel_required_string_update, diesel_required_url_update}, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; @@ -32,7 +33,7 @@ pub async fn update_oauth_provider( auto_verify_email: data.auto_verify_email, account_linking_enabled: data.account_linking_enabled, enabled: data.enabled, - updated: Some(Some(naive_now())), + updated: Some(Some(Utc::now())), }; let update_result = diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 16932cacb..948a7617e 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -12,17 +12,15 @@ use lemmy_api_common::{ get_url_blocklist, honeypot_check, local_site_to_slur_regex, - mark_post_as_read, process_markdown_opt, }, }; use lemmy_db_schema::{ - impls::actor_language::default_post_language, + impls::actor_language::validate_post_language, source::{ - actor_language::CommunityLanguage, community::Community, local_site::LocalSite, - post::{Post, PostInsertForm, PostLike, PostLikeForm}, + post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostReadForm}, }, traits::{Crud, Likeable}, utils::diesel_url_create, @@ -98,23 +96,13 @@ pub async fn create_post( .await?; } - // attempt to set default language if none was provided - let language_id = match data.language_id { - Some(lid) => lid, - None => { - default_post_language( - &mut context.pool(), - community.id, - local_user_view.local_user.id, - ) - .await? - } - }; - - // Only need to check if language is allowed in case user set it explicitly. When using default - // language, it already only returns allowed languages. - CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community.id) - .await?; + let language_id = validate_post_language( + &mut context.pool(), + data.language_id, + data.community_id, + local_user_view.local_user.id, + ) + .await?; let scheduled_publish_time = convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?; @@ -154,17 +142,14 @@ pub async fn create_post( // They like their own post by default let person_id = local_user_view.person.id; let post_id = inserted_post.id; - let like_form = PostLikeForm { - post_id, - person_id, - score: 1, - }; + let like_form = PostLikeForm::new(post_id, person_id, 1); PostLike::like(&mut context.pool(), &like_form) .await .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; - mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + let read_form = PostReadForm::new(post_id, person_id); + PostRead::mark_as_read(&mut context.pool(), &read_form).await?; build_post_response(&context, community_id, local_user_view, post_id).await } diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 7677d59ef..3b6ef9414 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -2,10 +2,13 @@ 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, update_read_comments}, + utils::{check_private_instance, is_mod_or_admin_opt, update_read_comments}, }; use lemmy_db_schema::{ - source::{comment::Comment, post::Post}, + source::{ + comment::Comment, + post::{Post, PostRead, PostReadForm}, + }, traits::Crud, }; use lemmy_db_views::{ @@ -62,7 +65,8 @@ pub async fn get_post( 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?; + let read_form = PostReadForm::new(post_id, person_id); + PostRead::mark_as_read(&mut context.pool(), &read_form).await?; update_read_comments( person_id, diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index 7e3261e6f..95aa5fc56 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ source::{ community::Community, local_user::LocalUser, - moderator::{ModRemovePost, ModRemovePostForm}, + mod_log::moderator::{ModRemovePost, ModRemovePostForm}, post::{Post, PostUpdateForm}, post_report::PostReport, }, diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index fc23e7d9e..24bbed009 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -1,6 +1,7 @@ use super::{convert_published_time, create::send_webmention}; use activitypub_federation::config::Data; use actix_web::web::Json; +use chrono::Utc; use lemmy_api_common::{ build_response::build_post_response, context::LemmyContext, @@ -15,14 +16,14 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ + impls::actor_language::validate_post_language, source::{ - actor_language::CommunityLanguage, community::Community, local_site::LocalSite, post::{Post, PostUpdateForm}, }, traits::Crud, - utils::{diesel_string_update, diesel_url_update, naive_now}, + utils::{diesel_string_update, diesel_url_update}, }; use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::{ @@ -101,14 +102,13 @@ pub async fn update_post( Err(LemmyErrorType::NoPostEditAllowed)? } - if let Some(language_id) = data.language_id { - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - language_id, - orig_post.community.id, - ) - .await?; - } + let language_id = validate_post_language( + &mut context.pool(), + data.language_id, + orig_post.post.community_id, + local_user_view.local_user.id, + ) + .await?; // handle changes to scheduled_publish_time let scheduled_publish_time = match ( @@ -131,8 +131,8 @@ pub async fn update_post( body, alt_text, nsfw: data.nsfw, - language_id: data.language_id, - updated: Some(Some(naive_now())), + language_id: Some(language_id), + updated: Some(Some(Utc::now())), scheduled_publish_time, ..Default::default() }; diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index aa562c626..b9e4785ef 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -1,5 +1,6 @@ use activitypub_federation::config::Data; use actix_web::web::Json; +use chrono::Utc; use lemmy_api_common::{ context::LemmyContext, private_message::{EditPrivateMessage, PrivateMessageResponse}, @@ -12,7 +13,6 @@ use lemmy_db_schema::{ private_message::{PrivateMessage, PrivateMessageUpdateForm}, }, traits::Crud, - utils::naive_now, }; use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; use lemmy_utils::{ @@ -47,7 +47,7 @@ pub async fn update_private_message( private_message_id, &PrivateMessageUpdateForm { content: Some(content), - updated: Some(Some(naive_now())), + updated: Some(Some(Utc::now())), ..Default::default() }, ) diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index e1ea1d992..c8140cc28 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -2,6 +2,7 @@ use super::not_zero; use crate::site::{application_question_check, site_default_post_listing_type_check}; use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair}; use actix_web::web::Json; +use chrono::Utc; use lemmy_api_common::{ context::LemmyContext, site::{CreateSite, SiteResponse}, @@ -23,7 +24,7 @@ use lemmy_db_schema::{ site::{Site, SiteUpdateForm}, }, traits::Crud, - utils::{diesel_string_update, diesel_url_create, naive_now}, + utils::{diesel_string_update, diesel_url_create}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ @@ -75,7 +76,7 @@ pub async fn create_site( icon: Some(icon), banner: Some(banner), actor_id: Some(actor_id), - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), inbox_url, private_key: Some(Some(keypair.private_key)), public_key: Some(keypair.public_key), @@ -102,7 +103,7 @@ pub async fn create_site( legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, - updated: Some(Some(naive_now())), + updated: Some(Some(Utc::now())), slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()), actor_name_max_length: data.actor_name_max_length, federation_enabled: data.federation_enabled, @@ -161,7 +162,7 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> .slur_filter_regex .as_deref() .or(local_site.slur_filter_regex.as_deref()), - )?; + ); site_name_length_check(&create_site.name)?; check_slurs(&create_site.name, &slur_regex)?; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 47fd1f154..6bee0fda6 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -16,11 +16,11 @@ use lemmy_db_schema::source::{ use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView}; use lemmy_utils::{ - error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, - CACHE_DURATION_API, + build_cache, + error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, + CacheLock, VERSION, }; -use moka::future::Cache; use std::sync::LazyLock; #[tracing::instrument(skip(context))] @@ -28,41 +28,10 @@ pub async fn get_site( local_user_view: Option, context: Data, ) -> LemmyResult> { - static CACHE: LazyLock> = LazyLock::new(|| { - Cache::builder() - .max_capacity(1) - .time_to_live(CACHE_DURATION_API) - .build() - }); - // This data is independent from the user account so we can cache it across requests + static CACHE: CacheLock = LazyLock::new(build_cache); let mut site_response = CACHE - .try_get_with::<_, LemmyError>((), async { - let site_view = SiteView::read_local(&mut context.pool()).await?; - let admins = PersonView::admins(&mut context.pool()).await?; - let all_languages = Language::read_all(&mut context.pool()).await?; - let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; - let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; - let tagline = Tagline::get_random(&mut context.pool()).await.ok(); - let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?; - let oauth_providers = - OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone()); - - Ok(GetSiteResponse { - site_view, - admins, - version: VERSION.to_string(), - my_user: None, - all_languages, - discussion_languages, - blocked_urls, - tagline, - oauth_providers: Some(oauth_providers), - admin_oauth_providers: Some(admin_oauth_providers), - taglines: vec![], - custom_emojis: vec![], - }) - }) + .try_get_with((), read_site(&context)) .await .map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?; @@ -112,3 +81,29 @@ pub async fn get_site( Ok(Json(site_response)) } + +async fn read_site(context: &LemmyContext) -> LemmyResult { + let site_view = SiteView::read_local(&mut context.pool()).await?; + let admins = PersonView::admins(&mut context.pool()).await?; + let all_languages = Language::read_all(&mut context.pool()).await?; + let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; + let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; + let tagline = Tagline::get_random(&mut context.pool()).await.ok(); + let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?; + let oauth_providers = OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone()); + + Ok(GetSiteResponse { + site_view, + admins, + version: VERSION.to_string(), + my_user: None, + all_languages, + discussion_languages, + blocked_urls, + tagline, + oauth_providers: Some(oauth_providers), + admin_oauth_providers: Some(admin_oauth_providers), + taglines: vec![], + custom_emojis: vec![], + }) +} diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 085ed69d1..d2585ea43 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -2,6 +2,7 @@ use super::not_zero; use crate::site::{application_question_check, site_default_post_listing_type_check}; use activitypub_federation::config::Data; use actix_web::web::Json; +use chrono::Utc; use lemmy_api_common::{ context::LemmyContext, request::replace_image, @@ -18,8 +19,6 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ actor_language::SiteLanguage, - federation_allowlist::FederationAllowList, - federation_blocklist::FederationBlockList, local_site::{LocalSite, LocalSiteUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, local_site_url_blocklist::LocalSiteUrlBlocklist, @@ -27,7 +26,7 @@ use lemmy_db_schema::{ site::{Site, SiteUpdateForm}, }, traits::Crud, - utils::{diesel_string_update, diesel_url_update, naive_now}, + utils::{diesel_string_update, diesel_url_update}, RegistrationMode, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; @@ -88,7 +87,7 @@ pub async fn update_site( icon, banner, content_warning: diesel_string_update(data.content_warning.as_deref()), - updated: Some(Some(naive_now())), + updated: Some(Some(Utc::now())), ..Default::default() }; @@ -111,7 +110,7 @@ pub async fn update_site( legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, - updated: Some(Some(naive_now())), + updated: Some(Some(Utc::now())), slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()), actor_name_max_length: data.actor_name_max_length, federation_enabled: data.federation_enabled, @@ -151,12 +150,6 @@ pub async fn update_site( .await .ok(); - // Replace the blocked and allowed instances - let allowed = data.allowed_instances.clone(); - FederationAllowList::replace(&mut context.pool(), allowed).await?; - let blocked = data.blocked_instances.clone(); - FederationBlockList::replace(&mut context.pool(), blocked).await?; - if let Some(url_blocklist) = data.blocked_urls.clone() { let parsed_urls = check_urls_are_valid(&url_blocklist)?; LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?; @@ -210,7 +203,7 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm .slur_filter_regex .as_deref() .or(local_site.slur_filter_regex.as_deref()), - )?; + ); if let Some(name) = &edit_site.name { // The name doesn't need to be updated, but if provided it cannot be blanked out... diff --git a/crates/api_crud/src/tagline/update.rs b/crates/api_crud/src/tagline/update.rs index 043589d26..30b7d4370 100644 --- a/crates/api_crud/src/tagline/update.rs +++ b/crates/api_crud/src/tagline/update.rs @@ -1,5 +1,6 @@ use activitypub_federation::config::Data; use actix_web::web::Json; +use chrono::Utc; use lemmy_api_common::{ context::LemmyContext, tagline::{TaglineResponse, UpdateTagline}, @@ -11,7 +12,6 @@ use lemmy_db_schema::{ tagline::{Tagline, TaglineUpdateForm}, }, traits::Crud, - utils::naive_now, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; @@ -33,7 +33,7 @@ pub async fn update_tagline( let tagline_form = TaglineUpdateForm { content, - updated: naive_now(), + updated: Utc::now(), }; let tagline = Tagline::update(&mut context.pool(), data.id, &tagline_form).await?; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index ed560e3d6..deb65ec38 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -21,8 +21,9 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::PersonAggregates, - newtypes::{InstanceId, OAuthProviderId}, + newtypes::{InstanceId, OAuthProviderId, SiteId}, source::{ + actor_language::SiteLanguage, captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, language::Language, local_site::LocalSite, @@ -145,17 +146,24 @@ pub async fn register( ..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string())) }; - let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?; + let inserted_local_user = create_local_user( + &context, + language_tags, + &local_user_form, + local_site.site_id, + ) + .await?; if local_site.site_setup && require_registration_application { - // Create the registration application - let form = RegistrationApplicationInsertForm { - local_user_id: inserted_local_user.id, - // We already made sure answer was not null above - answer: data.answer.clone().expect("must have an answer"), - }; + if let Some(answer) = data.answer.clone() { + // Create the registration application + let form = RegistrationApplicationInsertForm { + local_user_id: inserted_local_user.id, + answer, + }; - RegistrationApplication::create(&mut context.pool(), &form).await?; + RegistrationApplication::create(&mut context.pool(), &form).await?; + } } // Email the admins, only if email verification is not required @@ -304,7 +312,7 @@ pub async fn authenticate_with_oauth( OAuthAccount::create(&mut context.pool(), &oauth_account_form) .await - .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?; local_user = user_view.local_user.clone(); } else { @@ -357,7 +365,13 @@ pub async fn authenticate_with_oauth( ..LocalUserInsertForm::new(person.id, None) }; - local_user = create_local_user(&context, language_tags, &local_user_form).await?; + local_user = create_local_user( + &context, + language_tags, + &local_user_form, + local_site.site_id, + ) + .await?; // Create the oauth account let oauth_account_form = @@ -365,7 +379,7 @@ pub async fn authenticate_with_oauth( OAuthAccount::create(&mut context.pool(), &oauth_account_form) .await - .map_err(|_| LemmyErrorType::IncorrectLogin)?; + .with_lemmy_type(LemmyErrorType::IncorrectLogin)?; // prevent sign in until application is accepted if local_site.site_setup @@ -373,17 +387,19 @@ pub async fn authenticate_with_oauth( && !local_user.accepted_application && !local_user.admin { - // Create the registration application - RegistrationApplication::create( - &mut context.pool(), - &RegistrationApplicationInsertForm { - local_user_id: local_user.id, - answer: data.answer.clone().expect("must have an answer"), - }, - ) - .await?; + if let Some(answer) = data.answer.clone() { + // Create the registration application + RegistrationApplication::create( + &mut context.pool(), + &RegistrationApplicationInsertForm { + local_user_id: local_user.id, + answer, + }, + ) + .await?; - login_response.registration_created = true; + login_response.registration_created = true; + } } // Check email is verified when required @@ -446,15 +462,23 @@ async fn create_local_user( context: &Data, language_tags: Vec, local_user_form: &LocalUserInsertForm, + local_site_id: SiteId, ) -> Result { let all_languages = Language::read_all(&mut context.pool()).await?; // use hashset to avoid duplicates let mut language_ids = HashSet::new(); + + // Enable languages from `Accept-Language` header for l in language_tags { if let Some(found) = all_languages.iter().find(|all| all.code == l) { language_ids.insert(found.id); } } + + // Enable site languages. Ignored if all languages are enabled. + let discussion_languages = SiteLanguage::read(&mut context.pool(), local_site_id).await?; + language_ids.extend(discussion_languages); + let language_ids = language_ids.into_iter().collect(); let inserted_local_user = @@ -483,7 +507,7 @@ async fn send_verification_email_if_required( &local_user .email .clone() - .expect("invalid verification email"), + .ok_or(LemmyErrorType::EmailRequired)?, &mut context.pool(), context.settings(), ) @@ -524,18 +548,16 @@ async fn oauth_request_access_token( ("client_secret", &oauth_provider.client_secret), ]) .send() - .await; - - let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?; - if !response.status().is_success() { - Err(LemmyErrorType::OauthLoginFailed)?; - } + .await + .with_lemmy_type(LemmyErrorType::OauthLoginFailed)? + .error_for_status() + .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?; // Extract the access token let token_response = response .json::() .await - .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?; Ok(token_response) } @@ -552,18 +574,16 @@ async fn oidc_get_user_info( .header("Accept", "application/json") .bearer_auth(access_token) .send() - .await; - - let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?; - if !response.status().is_success() { - Err(LemmyErrorType::OauthLoginFailed)?; - } + .await + .with_lemmy_type(LemmyErrorType::OauthLoginFailed)? + .error_for_status() + .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?; // Extract the OAUTH user_id claim from the returned user_info let user_info = response .json::() .await - .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?; Ok(user_info) } @@ -571,7 +591,7 @@ async fn oidc_get_user_info( fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult { if let Some(value) = user_info.get(key) { let result = serde_json::from_value::(value.clone()) - .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?; return Ok(result); } Err(LemmyErrorType::OauthLoginFailed)? diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index 55eadeaf9..057e9bd38 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -42,7 +42,7 @@ reqwest = { workspace = true } moka.workspace = true serde_with.workspace = true html2md = "0.2.14" -html2text = "0.12.5" +html2text = "0.12.6" stringreader = "0.1.1" enum_delegate = "0.2.0" diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 866e1cc6c..64c402482 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -36,7 +36,7 @@ use lemmy_db_schema::{ CommunityPersonBan, CommunityPersonBanForm, }, - moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, person::{Person, PersonUpdateForm}, }, traits::{Bannable, Crud, Followable}, diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index 29fc22f0c..122eae429 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{CommunityPersonBan, CommunityPersonBanForm}, - moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, person::{Person, PersonUpdateForm}, }, traits::{Bannable, Crud}, diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index d32b9d76e..950f4861d 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -130,7 +130,7 @@ impl AnnounceActivity { actor: c.actor.clone().into_inner(), other: serde_json::to_value(c.object)? .as_object() - .expect("is object") + .ok_or(FederationError::Unreachable)? .clone(), }; let announce_compat = AnnounceActivity::new(announcable_page, community, context)?; @@ -215,7 +215,7 @@ async fn can_accept_activity_in_community( ) -> LemmyResult<()> { if let Some(community) = community { // Local only community can't federate - if community.visibility != CommunityVisibility::Public { + if community.visibility == CommunityVisibility::LocalOnly { return Err(LemmyErrorType::NotFound.into()); } if !community.local { diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs index ae508c2c5..1014229c8 100644 --- a/crates/apub/src/activities/community/collection_add.rs +++ b/crates/apub/src/activities/community/collection_add.rs @@ -31,7 +31,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, person::Person, post::{Post, PostUpdateForm}, }, diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs index 6c08735ed..c94286703 100644 --- a/crates/apub/src/activities/community/collection_remove.rs +++ b/crates/apub/src/activities/community/collection_remove.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, post::{Post, PostUpdateForm}, }, traits::{Crud, Joinable}, diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs index a9bacea8a..af6a5796f 100644 --- a/crates/apub/src/activities/community/lock_page.rs +++ b/crates/apub/src/activities/community/lock_page.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::Community, - moderator::{ModLockPost, ModLockPostForm}, + mod_log::moderator::{ModLockPost, ModLockPostForm}, person::Person, post::{Post, PostUpdateForm}, }, diff --git a/crates/apub/src/activities/community/mod.rs b/crates/apub/src/activities/community/mod.rs index 59b8fadbb..93c6e5c77 100644 --- a/crates/apub/src/activities/community/mod.rs +++ b/crates/apub/src/activities/community/mod.rs @@ -42,7 +42,7 @@ pub(crate) async fn send_activity_in_community( context: &Data, ) -> LemmyResult<()> { // If community is local only, don't send anything out - if community.visibility != CommunityVisibility::Public { + if community.visibility == CommunityVisibility::LocalOnly { return Ok(()); } diff --git a/crates/apub/src/activities/community/report.rs b/crates/apub/src/activities/community/report.rs index 4966add34..c33064459 100644 --- a/crates/apub/src/activities/community/report.rs +++ b/crates/apub/src/activities/community/report.rs @@ -70,7 +70,8 @@ impl Report { let object_creator = Person::read(&mut context.pool(), object_creator_id).await?; let object_creator_site: Option = Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id) - .await? + .await + .ok() .map(Into::into); if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) { inboxes.add_inbox(inbox); diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index 85be94246..fadf918bd 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -17,6 +17,7 @@ use activitypub_federation::{ kinds::activity::UpdateType, traits::{ActivityHandler, Actor, Object}, }; +use chrono::Utc; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ source::{ @@ -25,7 +26,6 @@ use lemmy_db_schema::{ person::Person, }, traits::Crud, - utils::naive_now, }; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; @@ -103,7 +103,7 @@ impl ActivityHandler for UpdateCommunity { nsfw: Some(self.object.sensitive.unwrap_or(false)), actor_id: Some(self.object.id.into()), public_key: Some(self.object.public_key.public_key_pem), - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), icon: Some(self.object.icon.map(|i| i.url.into())), banner: Some(self.object.image.map(|i| i.url.into())), followers_url: self.object.followers.map(Into::into), diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index 90ab0153f..9f64e805b 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -171,6 +171,9 @@ impl ActivityHandler for CreateOrUpdateNote { // TODO: for compatibility with other projects, it would be much better to read this from cc or // tags let mentions = scrape_text_for_mentions(&comment.content); + + // TODO: this fails in local community comment as CommentView::read() returns nothing + // without passing LocalUser send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).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 d0cf17a51..832b2da6d 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -118,11 +118,7 @@ impl ActivityHandler for CreateOrUpdatePage { let post = ApubPost::from_json(self.object, context).await?; // author likes their own post by default - let like_form = PostLikeForm { - post_id: post.id, - person_id: post.creator_id, - score: 1, - }; + let like_form = PostLikeForm::new(post.id, post.creator_id, 1); PostLike::like(&mut context.pool(), &like_form).await?; // Calculate initial hot_rank for post diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index 064f0bc82..4ad24d966 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -14,7 +14,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, community::{Community, CommunityUpdateForm}, - moderator::{ + mod_log::moderator::{ ModRemoveComment, ModRemoveCommentForm, ModRemoveCommunity, diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index f4a7bb9b9..b30b22fd4 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ source::{ comment::{Comment, CommentUpdateForm}, community::{Community, CommunityUpdateForm}, - moderator::{ + mod_log::moderator::{ ModRemoveComment, ModRemoveCommentForm, ModRemoveCommunity, diff --git a/crates/apub/src/activities/voting/mod.rs b/crates/apub/src/activities/voting/mod.rs index 7c39b2246..5cda291eb 100644 --- a/crates/apub/src/activities/voting/mod.rs +++ b/crates/apub/src/activities/voting/mod.rs @@ -79,11 +79,7 @@ async fn vote_post( context: &Data, ) -> LemmyResult<()> { let post_id = post.id; - let like_form = PostLikeForm { - post_id: post.id, - person_id: actor.id, - score: vote_type.into(), - }; + let like_form = PostLikeForm::new(post.id, actor.id, vote_type.into()); let person_id = actor.id; PostLike::remove(&mut context.pool(), person_id, post_id).await?; PostLike::like(&mut context.pool(), &like_form).await?; diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index cdf24dbaa..63e737fdd 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -10,7 +10,10 @@ use lemmy_api_common::{ post::{GetPosts, GetPostsResponse}, utils::{check_conflicting_like_filters, check_private_instance}, }; -use lemmy_db_schema::source::community::Community; +use lemmy_db_schema::{ + newtypes::PostId, + source::{community::Community, post::PostRead}, +}; use lemmy_db_views::{ post_view::PostQuery, structs::{LocalUserView, PaginationCursor, SiteView}, @@ -90,6 +93,17 @@ pub async fn list_posts( .await .with_lemmy_type(LemmyErrorType::CouldntGetPosts)?; + // If in their user settings (or as part of the API request), auto-mark fetched posts as read + if let Some(local_user) = local_user { + if data + .mark_as_read + .unwrap_or(local_user.auto_mark_fetched_posts_as_read) + { + let post_ids = posts.iter().map(|p| p.post.id).collect::>(); + PostRead::mark_many_as_read(&mut context.pool(), &post_ids, local_user.person_id).await?; + } + } + // if this page wasn't empty, then there is a next page after the last post on this page let next_page = posts.last().map(PaginationCursor::after_post); Ok(Json(GetPostsResponse { posts, next_page })) diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 601ba8664..6184df7d3 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -200,10 +200,7 @@ pub async fn import_settings( &context, |(saved, context)| async move { let post = saved.dereference(&context).await?; - let form = PostSavedForm { - person_id, - post_id: post.id, - }; + let form = PostSavedForm::new(post.id, person_id); PostSaved::save(&mut context.pool(), &form).await?; LemmyResult::Ok(()) }, @@ -325,7 +322,7 @@ pub(crate) mod tests { CommunityFollowerState, CommunityInsertForm, }, - local_user::LocalUser, + person::Person, }, traits::{Crud, Followable}, }; @@ -379,8 +376,8 @@ pub(crate) mod tests { assert_eq!(follows.len(), 1); assert_eq!(follows[0].community.actor_id, community.actor_id); - LocalUser::delete(pool, export_user.local_user.id).await?; - LocalUser::delete(pool, import_user.local_user.id).await?; + Person::delete(pool, export_user.person.id).await?; + Person::delete(pool, import_user.person.id).await?; Ok(()) } @@ -415,8 +412,8 @@ pub(crate) mod tests { Some(LemmyErrorType::TooManyItems) ); - LocalUser::delete(pool, export_user.local_user.id).await?; - LocalUser::delete(pool, import_user.local_user.id).await?; + Person::delete(pool, export_user.person.id).await?; + Person::delete(pool, import_user.person.id).await?; Ok(()) } diff --git a/crates/apub/src/fetcher/mod.rs b/crates/apub/src/fetcher/mod.rs index 29202004f..b2bc35672 100644 --- a/crates/apub/src/fetcher/mod.rs +++ b/crates/apub/src/fetcher/mod.rs @@ -5,7 +5,7 @@ use activitypub_federation::{ }; use diesel::NotFound; use itertools::Itertools; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{context::LemmyContext, LemmyErrorType}; use lemmy_db_schema::traits::ApubActor; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyError, LemmyResult}; @@ -42,7 +42,7 @@ where let (name, domain) = identifier .splitn(2, '@') .collect_tuple() - .expect("invalid query"); + .ok_or(LemmyErrorType::InvalidUrl)?; let actor = DbActor::read_from_name_and_domain(&mut context.pool(), name, domain) .await .ok() diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 96a917d91..dbcc51258 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -6,28 +6,41 @@ use crate::{ community_moderators::ApubCommunityModerators, community_outbox::ApubCommunityOutbox, }, + fetcher::site_or_community_or_user::SiteOrCommunityOrUser, http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response}, objects::community::ApubCommunity, }; use activitypub_federation::{ + actix_web::signing_actor, config::Data, + fetch::object_id::ObjectId, traits::{Collection, Object}, }; -use actix_web::{web, HttpRequest, HttpResponse}; +use actix_web::{ + web::{Path, Query}, + HttpRequest, + HttpResponse, +}; use lemmy_api_common::context::LemmyContext; -use lemmy_db_schema::{source::community::Community, traits::ApubActor}; +use lemmy_db_schema::{source::community::Community, traits::ApubActor, CommunityVisibility}; +use lemmy_db_views_actor::structs::CommunityFollowerView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serde::Deserialize; #[derive(Deserialize, Clone)] -pub(crate) struct CommunityQuery { +pub(crate) struct CommunityPath { community_name: String, } +#[derive(Deserialize, Clone)] +pub struct CommunityIsFollowerQuery { + is_follower: Option>, +} + /// Return the ActivityPub json representation of a local community over HTTP. #[tracing::instrument(skip_all)] pub(crate) async fn get_apub_community_http( - info: web::Path, + info: Path, context: Data, ) -> LemmyResult { let community: ApubCommunity = @@ -47,21 +60,59 @@ pub(crate) async fn get_apub_community_http( /// Returns an empty followers collection, only populating the size (for privacy). pub(crate) async fn get_apub_community_followers( - info: web::Path, + info: Path, + query: Query, context: Data, + request: HttpRequest, ) -> LemmyResult { let community = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? .ok_or(LemmyErrorType::NotFound)?; + if let Some(is_follower) = &query.is_follower { + return check_is_follower(community, is_follower, context, request).await; + } check_community_fetchable(&community)?; let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?; create_apub_response(&followers) } +/// Checks if a given actor follows the private community. Returns status 200 if true. +async fn check_is_follower( + community: Community, + is_follower: &ObjectId, + context: Data, + request: HttpRequest, +) -> LemmyResult { + if community.visibility != CommunityVisibility::Private { + return Ok(HttpResponse::BadRequest().body("must be a private community")); + } + // also check for http sig so that followers are not exposed publicly + let signing_actor = signing_actor::(&request, None, &context).await?; + CommunityFollowerView::check_has_followers_from_instance( + community.id, + signing_actor.instance_id(), + &mut context.pool(), + ) + .await?; + + let instance_id = is_follower.dereference(&context).await?.instance_id(); + let has_followers = CommunityFollowerView::check_has_followers_from_instance( + community.id, + instance_id, + &mut context.pool(), + ) + .await; + if has_followers.is_ok() { + Ok(HttpResponse::Ok().finish()) + } else { + Ok(HttpResponse::NotFound().finish()) + } +} + /// Returns the community outbox, which is populated by a maximum of 20 posts (but no other /// activities like votes or comments). pub(crate) async fn get_apub_community_outbox( - info: web::Path, + info: Path, context: Data, request: HttpRequest, ) -> LemmyResult { @@ -77,7 +128,7 @@ pub(crate) async fn get_apub_community_outbox( #[tracing::instrument(skip_all)] pub(crate) async fn get_apub_community_moderators( - info: web::Path, + info: Path, context: Data, ) -> LemmyResult { let community: ApubCommunity = @@ -92,7 +143,7 @@ pub(crate) async fn get_apub_community_moderators( /// Returns collection of featured (stickied) posts. pub(crate) async fn get_apub_community_featured( - info: web::Path, + info: Path, context: Data, request: HttpRequest, ) -> LemmyResult { @@ -181,17 +232,17 @@ pub(crate) mod tests { let request = TestRequest::default().to_http_request(); // fetch invalid community - let query = CommunityQuery { + let query = CommunityPath { community_name: "asd".to_string(), }; let res = get_apub_community_http(query.into(), context.reset_request_count()).await; assert!(res.is_err()); // fetch valid community - let query = CommunityQuery { + let path = CommunityPath { community_name: community.name.clone(), }; - let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?; + let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await?; assert_eq!(200, res.status()); let res_group: Group = decode_response(res).await?; let community: ApubCommunity = community.into(); @@ -199,20 +250,26 @@ pub(crate) mod tests { assert_eq!(group, res_group); let res = get_apub_community_featured( - query.clone().into(), + path.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await?; + assert_eq!(200, res.status()); + let query = Query(CommunityIsFollowerQuery { is_follower: None }); + let res = get_apub_community_followers( + path.clone().into(), + query, context.reset_request_count(), request.clone(), ) .await?; assert_eq!(200, res.status()); let res = - get_apub_community_followers(query.clone().into(), context.reset_request_count()).await?; + get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await?; assert_eq!(200, res.status()); let res = - get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?; - assert_eq!(200, res.status()); - let res = - get_apub_community_outbox(query.into(), context.reset_request_count(), request).await?; + get_apub_community_outbox(path.into(), context.reset_request_count(), request).await?; assert_eq!(200, res.status()); Instance::delete(&mut context.pool(), instance.id).await?; @@ -227,28 +284,35 @@ pub(crate) mod tests { let request = TestRequest::default().to_http_request(); // should return tombstone - let query = CommunityQuery { + let path: Path = CommunityPath { community_name: community.name.clone(), - }; - let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?; + } + .into(); + let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await?; assert_eq!(410, res.status()); let res_tombstone = decode_response::(res).await; assert!(res_tombstone.is_ok()); let res = get_apub_community_featured( - query.clone().into(), + path.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await; + assert!(res.is_err()); + let query = Query(CommunityIsFollowerQuery { is_follower: None }); + let res = get_apub_community_followers( + path.clone().into(), + query, context.reset_request_count(), request.clone(), ) .await; assert!(res.is_err()); let res = - get_apub_community_followers(query.clone().into(), context.reset_request_count()).await; + get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = - get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await; - assert!(res.is_err()); - let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await; + let res = get_apub_community_outbox(path, context.reset_request_count(), request).await; assert!(res.is_err()); //Community::delete(&mut context.pool(), community.id).await?; @@ -263,25 +327,32 @@ pub(crate) mod tests { let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?; let request = TestRequest::default().to_http_request(); - let query = CommunityQuery { + let path: Path = CommunityPath { community_name: community.name.clone(), - }; - let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await; + } + .into(); + let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); let res = get_apub_community_featured( - query.clone().into(), + path.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await; + assert!(res.is_err()); + let query = Query(CommunityIsFollowerQuery { is_follower: None }); + let res = get_apub_community_followers( + path.clone().into(), + query, context.reset_request_count(), request.clone(), ) .await; assert!(res.is_err()); let res = - get_apub_community_followers(query.clone().into(), context.reset_request_count()).await; + get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = - get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await; - assert!(res.is_err()); - let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await; + let res = get_apub_community_outbox(path, context.reset_request_count(), request).await; assert!(res.is_err()); Instance::delete(&mut context.pool(), instance.id).await?; diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index d79cd3d55..fc2fbf0d3 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -8,6 +8,7 @@ use activitypub_federation::{ actix_web::{inbox::receive_activity, signing_actor}, config::Data, protocol::context::WithContext, + traits::Actor, FEDERATION_CONTENT_TYPE, }; use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; @@ -18,7 +19,7 @@ use lemmy_db_schema::{ CommunityVisibility, }; use lemmy_db_views_actor::structs::CommunityFollowerView; -use lemmy_utils::error::{FederationError, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{FederationError, LemmyErrorExt, LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; use std::{ops::Deref, time::Duration}; use tokio::time::timeout; @@ -46,7 +47,7 @@ pub async fn shared_inbox( // consider the activity broken and move on. timeout(INCOMING_ACTIVITY_TIMEOUT, receive_fut) .await - .map_err(|_| FederationError::InboxTimeout)? + .with_lemmy_type(FederationError::InboxTimeout.into())? } /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub @@ -109,7 +110,7 @@ pub(crate) async fn get_activity( .into(); let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id) .await - .map_err(|_| FederationError::CouldntFindActivity)?; + .with_lemmy_type(FederationError::CouldntFindActivity.into())?; let sensitive = activity.sensitive; if sensitive { @@ -145,14 +146,27 @@ async fn check_community_content_fetchable( // from the fetching instance then fetching is allowed Private => { let signing_actor = signing_actor::(request, None, context).await?; - Ok( - CommunityFollowerView::check_has_followers_from_instance( - community.id, - signing_actor.instance_id(), - &mut context.pool(), + if community.local { + Ok( + CommunityFollowerView::check_has_followers_from_instance( + community.id, + signing_actor.instance_id(), + &mut context.pool(), + ) + .await?, ) - .await?, - ) + } else if let Some(followers_url) = community.followers_url.clone() { + let mut followers_url = followers_url.inner().clone(); + followers_url + .query_pairs_mut() + .append_pair("is_follower", signing_actor.id().as_str()); + let req = context.client().get(followers_url.as_str()); + let req = context.sign_request(req, Bytes::new()).await?; + context.client().execute(req).await?.error_for_status()?; + Ok(()) + } else { + Err(LemmyErrorType::NotFound.into()) + } } } } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index e11475d6c..028d673c2 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -11,6 +11,7 @@ use lemmy_db_schema::{ }; use lemmy_utils::{ error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}, + CacheLock, CACHE_DURATION_FEDERATION, }; use moka::future::Cache; @@ -50,7 +51,8 @@ impl UrlVerifier for VerifyUrlData { async fn verify(&self, url: &Url) -> Result<(), ActivityPubError> { let local_site_data = local_site_data_cached(&mut (&self.0).into()) .await - .expect("read local site data"); + .map_err(|e| ActivityPubError::Other(format!("Cant read local site data: {e}")))?; + use FederationError::*; check_apub_id_valid(url, &local_site_data).map_err(|err| match err { LemmyError { @@ -138,7 +140,7 @@ pub(crate) async fn local_site_data_cached( // multiple times. This causes a huge number of database reads if we hit the db directly. So we // cache these values for a short time, which will already make a huge difference and ensures that // changes take effect quickly. - static CACHE: LazyLock>> = LazyLock::new(|| { + static CACHE: CacheLock> = LazyLock::new(|| { Cache::builder() .max_capacity(1) .time_to_live(CACHE_DURATION_FEDERATION) @@ -176,10 +178,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness( .domain() .ok_or(FederationError::UrlWithoutDomain)? .to_string(); - let local_instance = context - .settings() - .get_hostname_without_port() - .expect("local hostname is valid"); + let local_instance = context.settings().get_hostname_without_port()?; if domain == local_instance { return Ok(()); } @@ -196,10 +195,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness( .iter() .map(|i| i.domain.clone()) .collect::>(); - let local_instance = context - .settings() - .get_hostname_without_port() - .expect("local hostname is valid"); + let local_instance = context.settings().get_hostname_without_port()?; allowed_and_local.push(local_instance); let domain = apub_id diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index b7c6a5f51..dc0721404 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -30,7 +30,6 @@ use lemmy_db_schema::{ post::Post, }, traits::Crud, - utils::naive_now, }; use lemmy_utils::{ error::{FederationError, LemmyError, LemmyResult}, @@ -204,7 +203,7 @@ impl Object for ApubComment { language_id, }; let parent_comment_path = parent_comment.map(|t| t.0.path); - let timestamp: DateTime = note.updated.or(note.published).unwrap_or_else(naive_now); + let timestamp: DateTime = note.updated.or(note.published).unwrap_or_else(Utc::now); let comment = Comment::insert_apub( &mut context.pool(), Some(timestamp), diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index efa2c5247..689641910 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -38,7 +38,6 @@ use lemmy_db_schema::{ local_site::LocalSite, }, traits::{ApubActor, Crud}, - utils::naive_now, CommunityVisibility, }; use lemmy_db_views_actor::structs::CommunityFollowerView; @@ -166,7 +165,7 @@ impl Object for ApubCommunity { nsfw: Some(group.sensitive.unwrap_or(false)), actor_id: Some(group.id.into()), local: Some(false), - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), icon, banner, sidebar, @@ -193,11 +192,17 @@ impl Object for ApubCommunity { let languages = LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).await?; - let timestamp = group.updated.or(group.published).unwrap_or_else(naive_now); - let community = Community::insert_apub(&mut context.pool(), timestamp, &form).await?; + let timestamp = group.updated.or(group.published).unwrap_or_else(Utc::now); + let community: ApubCommunity = Community::insert_apub(&mut context.pool(), timestamp, &form) + .await? + .into(); CommunityLanguage::update(&mut context.pool(), languages, community.id).await?; - let community: ApubCommunity = community.into(); + // Need to fetch mods synchronously, otherwise fetching a post in community with + // `posting_restricted_to_mods` can fail if mods havent been fetched yet. + if let Some(moderators) = group.attributed_to { + moderators.dereference(&community, context).await.ok(); + } // These collections are not necessary for Lemmy to work, so ignore errors. let community_ = community.clone(); @@ -210,9 +215,6 @@ impl Object for ApubCommunity { if let Some(featured) = group.featured { featured.dereference(&community_, &context_).await.ok(); } - if let Some(moderators) = group.attributed_to { - moderators.dereference(&community_, &context_).await.ok(); - } Ok(()) }); diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index a123c85ba..754172fe2 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -39,7 +39,6 @@ use lemmy_db_schema::{ site::{Site, SiteInsertForm}, }, traits::Crud, - utils::naive_now, }; use lemmy_utils::{ error::{FederationError, LemmyError, LemmyResult}, @@ -163,7 +162,7 @@ impl Object for ApubSite { banner, description: apub.summary, actor_id: Some(apub.id.clone().into()), - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), inbox_url: Some(apub.inbox.clone().into()), public_key: Some(apub.public_key.public_key_pem.clone()), private_key: None, diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 737579662..97b83c194 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -35,7 +35,6 @@ use lemmy_db_schema::{ person::{Person as DbPerson, PersonInsertForm, PersonUpdateForm}, }, traits::{ApubActor, Crud}, - utils::naive_now, }; use lemmy_utils::{ error::{LemmyError, LemmyResult}, @@ -176,7 +175,7 @@ impl Object for ApubPerson { bot_account: Some(person.kind == UserTypes::Service), private_key: None, public_key: person.public_key.public_key_pem, - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), inbox_url: Some( person .endpoints diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index b72fa1728..bcd1dbf8e 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -35,7 +35,6 @@ use lemmy_db_schema::{ post::{Post, PostInsertForm, PostUpdateForm}, }, traits::Crud, - utils::naive_now, }; use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_utils::{ @@ -260,7 +259,7 @@ impl Object for ApubPost { ..PostInsertForm::new(name, creator.id, community.id) }; - let timestamp = page.updated.or(page.published).unwrap_or_else(naive_now); + let timestamp = page.updated.or(page.published).unwrap_or_else(Utc::now); let post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?; let post_ = post.clone(); let context_ = context.reset_request_count(); diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index f3a9f140c..9ada5f657 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -31,7 +31,6 @@ use lemmy_db_schema::{ private_message::{PrivateMessage, PrivateMessageInsertForm}, }, traits::Crud, - utils::naive_now, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ @@ -161,7 +160,7 @@ impl Object for ApubPrivateMessage { ap_id: Some(note.id.into()), local: Some(false), }; - let timestamp = note.updated.or(note.published).unwrap_or_else(naive_now); + let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now); let pm = PrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?; Ok(pm.into()) } diff --git a/crates/db_perf/src/series.rs b/crates/db_perf/src/series.rs index 8efc078b1..eaa454702 100644 --- a/crates/db_perf/src/series.rs +++ b/crates/db_perf/src/series.rs @@ -60,9 +60,9 @@ impl> SelectableExpression for Valu impl> Insertable for ValuesFromSeries where - dsl::BareSelect: AsQuery + Insertable, + dsl::select: AsQuery + Insertable, { - type Values = as Insertable>::Values; + type Values = as Insertable>::Values; fn values(self) -> Self::Values { dsl::select(self).values() diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index c9b2a7930..eac9d6ddd 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -37,6 +37,8 @@ full = [ "tokio-postgres-rustls", "rustls", "i-love-jesus", + "tuplex", + "diesel-bind-if-some", ] [dependencies] @@ -73,11 +75,12 @@ tokio = { workspace = true, optional = true } tokio-postgres = { workspace = true, optional = true } tokio-postgres-rustls = { workspace = true, optional = true } rustls = { workspace = true, optional = true } -uuid = { workspace = true, features = ["v4"] } +uuid.workspace = true i-love-jesus = { workspace = true, optional = true } anyhow = { workspace = true } -moka.workspace = true +diesel-bind-if-some = { workspace = true, optional = true } derive-new.workspace = true +tuplex = { workspace = true, optional = true } [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 973d3325f..e5b3e22d0 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -38,7 +38,7 @@ AS $a$ BEGIN EXECUTE replace($b$ -- When a thing gets a vote, update its aggregates and its creator's aggregates - CALL r.create_triggers ('thing_like', $$ + CALL r.create_triggers ('thing_actions', $$ BEGIN WITH thing_diff AS ( UPDATE thing_aggregates AS a @@ -46,7 +46,8 @@ BEGIN score = a.score + diff.upvotes - diff.downvotes, upvotes = a.upvotes + diff.upvotes, downvotes = a.downvotes + diff.downvotes, controversy_rank = r.controversy_rank ((a.upvotes + diff.upvotes)::numeric, (a.downvotes + diff.downvotes)::numeric) FROM ( SELECT - (thing_like).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (thing_like).thing_id) AS diff + (thing_actions).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_actions).like_score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_actions).like_score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows + WHERE (thing_actions).like_score IS NOT NULL GROUP BY (thing_actions).thing_id) AS diff WHERE a.thing_id = diff.thing_id AND (diff.upvotes, diff.downvotes) != (0, 0) @@ -360,7 +361,7 @@ CREATE TRIGGER comment_count -- Count subscribers for communities. -- subscribers should be updated only when a local community is followed by a local or remote person. -- subscribers_local should be updated only when a local person follows a local or remote community. -CALL r.create_triggers ('community_follower', $$ +CALL r.create_triggers ('community_actions', $$ BEGIN UPDATE community_aggregates AS a @@ -368,10 +369,11 @@ BEGIN subscribers = a.subscribers + diff.subscribers, subscribers_local = a.subscribers_local + diff.subscribers_local FROM ( SELECT - (community_follower).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local + (community_actions).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local FROM select_old_and_new_rows AS old_and_new_rows - LEFT JOIN community ON community.id = (community_follower).community_id - LEFT JOIN person ON person.id = (community_follower).person_id GROUP BY (community_follower).community_id) AS diff + LEFT JOIN community ON community.id = (community_actions).community_id + LEFT JOIN person ON person.id = (community_actions).person_id + WHERE (community_actions).followed IS NOT NULL GROUP BY (community_actions).community_id) AS diff WHERE a.community_id = diff.community_id AND (diff.subscribers, diff.subscribers_local) != (0, 0); @@ -382,6 +384,44 @@ END; $$); +CALL r.create_triggers ('post_report', $$ +BEGIN + UPDATE + post_aggregates AS a + SET + report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count + FROM ( + SELECT + (post_report).post_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (post_report).resolved), 0) AS unresolved_report_count + FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (post_report).post_id) AS diff +WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0) + AND a.post_id = diff.post_id; + +RETURN NULL; + +END; + +$$); + +CALL r.create_triggers ('comment_report', $$ +BEGIN + UPDATE + comment_aggregates AS a + SET + report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count + FROM ( + SELECT + (comment_report).comment_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (comment_report).resolved), 0) AS unresolved_report_count + FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (comment_report).comment_id) AS diff +WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0) + AND a.comment_id = diff.comment_id; + +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 () @@ -541,7 +581,7 @@ CREATE FUNCTION r.delete_follow_before_person () LANGUAGE plpgsql AS $$ BEGIN - DELETE FROM community_follower AS c + DELETE FROM community_actions AS c WHERE c.person_id = OLD.id; RETURN OLD; END; diff --git a/crates/db_schema/replaceable_schema/utils.sql b/crates/db_schema/replaceable_schema/utils.sql index c766d25f2..0c7f42ff2 100644 --- a/crates/db_schema/replaceable_schema/utils.sql +++ b/crates/db_schema/replaceable_schema/utils.sql @@ -151,3 +151,118 @@ DECLARE END; $a$; +-- Edit community aggregates to include voters as active users +CREATE OR REPLACE FUNCTION r.community_aggregates_activity (i text) + RETURNS TABLE ( + count_ bigint, + community_id_ integer) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN query + SELECT + count(*), + community_id + FROM ( + SELECT + c.creator_id, + p.community_id + FROM + comment c + INNER JOIN post p ON c.post_id = p.id + INNER JOIN person pe ON c.creator_id = pe.id + WHERE + c.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + p.creator_id, + p.community_id + FROM + post p + INNER JOIN person pe ON p.creator_id = pe.id + WHERE + p.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + pl.person_id, + p.community_id + FROM + post_like pl + INNER JOIN post p ON pl.post_id = p.id + INNER JOIN person pe ON pl.person_id = pe.id + WHERE + pl.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + cl.person_id, + p.community_id + FROM + comment_like cl + INNER JOIN comment c ON cl.comment_id = c.id + INNER JOIN post p ON c.post_id = p.id + INNER JOIN person pe ON cl.person_id = pe.id + WHERE + cl.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE) a +GROUP BY + community_id; +END; +$$; + +-- Edit site aggregates to include voters and people who have read posts as active users +CREATE OR REPLACE FUNCTION r.site_aggregates_activity (i text) + RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE + count_ integer; +BEGIN + SELECT + count(*) INTO count_ + FROM ( + SELECT + c.creator_id + FROM + comment c + INNER JOIN person pe ON c.creator_id = pe.id + WHERE + c.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + p.creator_id + FROM + post p + INNER JOIN person pe ON p.creator_id = pe.id + WHERE + p.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + pl.person_id + FROM + post_like pl + INNER JOIN person pe ON pl.person_id = pe.id + WHERE + pl.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + cl.person_id + FROM + comment_like cl + INNER JOIN person pe ON cl.person_id = pe.id + WHERE + cl.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE) a; + RETURN count_; +END; +$$; + diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index 62aa9b609..df7004d0e 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -65,11 +65,7 @@ mod tests { ); let inserted_post = Post::create(pool, &new_post).await?; - let post_like = PostLikeForm { - post_id: inserted_post.id, - person_id: inserted_person.id, - score: 1, - }; + let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1); let _inserted_post_like = PostLike::like(pool, &post_like).await?; let comment_form = CommentInsertForm::new( diff --git a/crates/db_schema/src/aggregates/person_post_aggregates.rs b/crates/db_schema/src/aggregates/person_post_aggregates.rs index f6e108ee9..63a50af9c 100644 --- a/crates/db_schema/src/aggregates/person_post_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_post_aggregates.rs @@ -2,10 +2,17 @@ use crate::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, diesel::OptionalExtension, newtypes::{PersonId, PostId}, - schema::person_post_aggregates::dsl::{person_id, person_post_aggregates, post_id}, - utils::{get_conn, DbPool}, + schema::post_actions, + utils::{find_action, get_conn, now, DbPool}, +}; +use diesel::{ + expression::SelectableHelper, + insert_into, + result::Error, + ExpressionMethods, + NullableExpressionMethods, + QueryDsl, }; -use diesel::{insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; impl PersonPostAggregates { @@ -14,11 +21,13 @@ impl PersonPostAggregates { form: &PersonPostAggregatesForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_post_aggregates) + let form = (form, post_actions::read_comments.eq(now().nullable())); + insert_into(post_actions::table) .values(form) - .on_conflict((person_id, post_id)) + .on_conflict((post_actions::person_id, post_actions::post_id)) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -28,8 +37,8 @@ impl PersonPostAggregates { post_id_: PostId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - person_post_aggregates - .find((person_id_, post_id_)) + find_action(post_actions::read_comments, (person_id_, post_id_)) + .select(Self::as_select()) .first(conn) .await .optional() diff --git a/crates/db_schema/src/aggregates/post_aggregates.rs b/crates/db_schema/src/aggregates/post_aggregates.rs index 46747b076..c11ea6e05 100644 --- a/crates/db_schema/src/aggregates/post_aggregates.rs +++ b/crates/db_schema/src/aggregates/post_aggregates.rs @@ -113,11 +113,7 @@ mod tests { let inserted_child_comment = Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - let post_like = PostLikeForm { - post_id: inserted_post.id, - person_id: inserted_person.id, - score: 1, - }; + let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1); PostLike::like(pool, &post_like).await?; @@ -129,11 +125,7 @@ mod tests { assert_eq!(0, post_aggs_before_delete.downvotes); // Add a post dislike from the other person - let post_dislike = PostLikeForm { - post_id: inserted_post.id, - person_id: another_inserted_person.id, - score: -1, - }; + let post_dislike = PostLikeForm::new(inserted_post.id, another_inserted_person.id, -1); PostLike::like(pool, &post_dislike).await?; diff --git a/crates/db_schema/src/aggregates/structs.rs b/crates/db_schema/src/aggregates/structs.rs index fd7f70409..7a97666aa 100644 --- a/crates/db_schema/src/aggregates/structs.rs +++ b/crates/db_schema/src/aggregates/structs.rs @@ -4,12 +4,14 @@ use crate::schema::{ comment_aggregates, community_aggregates, person_aggregates, - person_post_aggregates, + post_actions, post_aggregates, site_aggregates, }; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; +#[cfg(feature = "full")] use i_love_jesus::CursorKeysModule; use serde::{Deserialize, Serialize}; #[cfg(feature = "full")] @@ -37,6 +39,8 @@ pub struct CommentAggregates { pub hot_rank: f64, #[serde(skip)] pub controversy_rank: f64, + pub report_count: i16, + pub unresolved_report_count: i16, } #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] @@ -144,6 +148,8 @@ pub struct PostAggregates { /// A rank that amplifies smaller communities #[serde(skip)] pub scaled_rank: f64, + pub report_count: i16, + pub unresolved_report_count: i16, } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] @@ -151,7 +157,7 @@ pub struct PostAggregates { feature = "full", derive(Queryable, Selectable, Associations, Identifiable) )] -#[cfg_attr(feature = "full", diesel(table_name = person_post_aggregates))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] @@ -162,18 +168,22 @@ pub struct PersonPostAggregates { /// The number of comments they've read on that post. /// /// This is updated to the current post comment count every time they view a post. + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read_comments_amount.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub read_comments: i64, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read_comments.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone, Default)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_post_aggregates))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PersonPostAggregatesForm { pub person_id: PersonId, pub post_id: PostId, + #[cfg_attr(feature = "full", diesel(column_name = read_comments_amount))] pub read_comments: i64, - pub published: Option>, } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy, Hash)] diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index b4ad0d347..ab322f0cd 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -197,7 +197,7 @@ impl SiteLanguage { impl CommunityLanguage { /// Returns true if the given language is one of configured languages for given community - pub async fn is_allowed_community_language( + async fn is_allowed_community_language( pool: &mut DbPool<'_>, for_language_id: LanguageId, for_community_id: CommunityId, @@ -319,29 +319,38 @@ impl CommunityLanguage { } } -pub async fn default_post_language( +pub async fn validate_post_language( pool: &mut DbPool<'_>, + language_id: Option, community_id: CommunityId, local_user_id: LocalUserId, -) -> Result { +) -> LemmyResult { use crate::schema::{community_language::dsl as cl, local_user_language::dsl as ul}; let conn = &mut get_conn(pool).await?; - let mut intersection = ul::local_user_language - .inner_join(cl::community_language.on(ul::language_id.eq(cl::language_id))) - .filter(ul::local_user_id.eq(local_user_id)) - .filter(cl::community_id.eq(community_id)) - .select(cl::language_id) - .get_results::(conn) - .await?; + let language_id = match language_id { + None | Some(LanguageId(0)) => { + let mut intersection = ul::local_user_language + .inner_join(cl::community_language.on(ul::language_id.eq(cl::language_id))) + .filter(ul::local_user_id.eq(local_user_id)) + .filter(cl::community_id.eq(community_id)) + .select(cl::language_id) + .get_results::(conn) + .await?; - if intersection.len() == 1 { - Ok(intersection.pop().unwrap_or(UNDETERMINED_ID)) - } else if intersection.len() == 2 && intersection.contains(&UNDETERMINED_ID) { - intersection.retain(|i| i != &UNDETERMINED_ID); - Ok(intersection.pop().unwrap_or(UNDETERMINED_ID)) - } else { - Ok(UNDETERMINED_ID) - } + if intersection.len() == 1 { + intersection.pop().unwrap_or(UNDETERMINED_ID) + } else if intersection.len() == 2 && intersection.contains(&UNDETERMINED_ID) { + intersection.retain(|i| i != &UNDETERMINED_ID); + intersection.pop().unwrap_or(UNDETERMINED_ID) + } else { + UNDETERMINED_ID + } + } + Some(lid) => lid, + }; + + CommunityLanguage::is_allowed_community_language(pool, language_id, community_id).await?; + Ok(language_id) } /// If no language is given, set all languages @@ -363,6 +372,7 @@ async fn convert_update_languages( } /// If all languages are returned, return empty vec instead +#[allow(clippy::expect_used)] async fn convert_read_languages( conn: &mut AsyncPgConnection, language_ids: Vec, @@ -501,7 +511,7 @@ mod tests { #[tokio::test] #[serial] - async fn test_user_languages() -> Result<(), Error> { + async fn test_user_languages() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); @@ -534,7 +544,7 @@ mod tests { #[tokio::test] #[serial] - async fn test_community_languages() -> Result<(), Error> { + async fn test_community_languages() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; @@ -590,7 +600,7 @@ mod tests { #[tokio::test] #[serial] - async fn test_default_post_language() -> Result<(), Error> { + async fn test_validate_post_language() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; @@ -613,8 +623,11 @@ mod tests { LocalUserLanguage::update(pool, test_langs2, local_user.id).await?; // no overlap in user/community languages, so defaults to undetermined - let def1 = default_post_language(pool, community.id, local_user.id).await?; - assert_eq!(UNDETERMINED_ID, def1); + let def1 = validate_post_language(pool, None, community.id, local_user.id).await; + assert_eq!( + Some(LemmyErrorType::LanguageNotAllowed), + def1.err().map(|e| e.error_type) + ); let ru = Language::read_id_from_code(pool, "ru").await?; let test_langs3 = vec![ @@ -626,7 +639,7 @@ mod tests { LocalUserLanguage::update(pool, test_langs3, local_user.id).await?; // this time, both have ru as common lang - let def2 = default_post_language(pool, community.id, local_user.id).await?; + let def2 = validate_post_language(pool, None, community.id, local_user.id).await?; assert_eq!(ru, def2); Person::delete(pool, person.id).await?; diff --git a/crates/db_schema/src/impls/captcha_answer.rs b/crates/db_schema/src/impls/captcha_answer.rs index 8be8fc5de..65e0d36f4 100644 --- a/crates/db_schema/src/impls/captcha_answer.rs +++ b/crates/db_schema/src/impls/captcha_answer.rs @@ -57,11 +57,12 @@ mod tests { source::captcha_answer::{CaptchaAnswer, CaptchaAnswerForm, CheckCaptchaAnswer}, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use serial_test::serial; #[tokio::test] #[serial] - async fn test_captcha_happy_path() { + async fn test_captcha_happy_path() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); @@ -71,8 +72,7 @@ mod tests { answer: "XYZ".to_string(), }, ) - .await - .expect("should not fail to insert captcha"); + .await?; let result = CaptchaAnswer::check_captcha( pool, @@ -84,11 +84,12 @@ mod tests { .await; assert!(result.is_ok()); + Ok(()) } #[tokio::test] #[serial] - async fn test_captcha_repeat_answer_fails() { + async fn test_captcha_repeat_answer_fails() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); @@ -98,8 +99,7 @@ mod tests { answer: "XYZ".to_string(), }, ) - .await - .expect("should not fail to insert captcha"); + .await?; let _result = CaptchaAnswer::check_captcha( pool, @@ -120,5 +120,7 @@ mod tests { .await; assert!(result_repeat.is_err()); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index d261dbf2c..7dcc033a1 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -1,7 +1,7 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommentId, DbUrl, PersonId}, - schema::comment, + schema::{comment, comment_actions}, source::comment::{ Comment, CommentInsertForm, @@ -12,10 +12,17 @@ use crate::{ CommentUpdateForm, }, traits::{Crud, Likeable, Saveable}, - utils::{functions::coalesce, get_conn, naive_now, DbPool, DELETED_REPLACEMENT_TEXT}, + utils::{functions::coalesce, get_conn, now, uplete, DbPool, DELETED_REPLACEMENT_TEXT}, }; use chrono::{DateTime, Utc}; -use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel::{ + dsl::insert_into, + expression::SelectableHelper, + result::Error, + ExpressionMethods, + NullableExpressionMethods, + QueryDsl, +}; use diesel_async::RunQueryDsl; use diesel_ltree::Ltree; use url::Url; @@ -31,7 +38,7 @@ impl Comment { .set(( comment::content.eq(DELETED_REPLACEMENT_TEXT), comment::deleted.eq(true), - comment::updated.eq(naive_now()), + comment::updated.eq(Utc::now()), )) .get_results::(conn) .await @@ -46,7 +53,7 @@ impl Comment { diesel::update(comment::table.filter(comment::creator_id.eq(for_creator_id))) .set(( comment::removed.eq(removed), - comment::updated.eq(naive_now()), + comment::updated.eq(Utc::now()), )) .get_results::(conn) .await @@ -141,13 +148,17 @@ impl Likeable for CommentLike { type Form = CommentLikeForm; type IdType = CommentId; async fn like(pool: &mut DbPool<'_>, comment_like_form: &CommentLikeForm) -> Result { - use crate::schema::comment_like::dsl::{comment_id, comment_like, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(comment_like) + let comment_like_form = ( + comment_like_form, + comment_actions::liked.eq(now().nullable()), + ); + insert_into(comment_actions::table) .values(comment_like_form) - .on_conflict((comment_id, person_id)) + .on_conflict((comment_actions::comment_id, comment_actions::person_id)) .do_update() .set(comment_like_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -155,11 +166,12 @@ impl Likeable for CommentLike { pool: &mut DbPool<'_>, person_id: PersonId, comment_id: CommentId, - ) -> Result { - use crate::schema::comment_like::dsl::comment_like; + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(comment_like.find((person_id, comment_id))) - .execute(conn) + uplete::new(comment_actions::table.find((person_id, comment_id))) + .set_null(comment_actions::like_score) + .set_null(comment_actions::liked) + .get_result(conn) .await } } @@ -171,26 +183,30 @@ impl Saveable for CommentSaved { pool: &mut DbPool<'_>, comment_saved_form: &CommentSavedForm, ) -> Result { - use crate::schema::comment_saved::dsl::{comment_id, comment_saved, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(comment_saved) + let comment_saved_form = ( + comment_saved_form, + comment_actions::saved.eq(now().nullable()), + ); + insert_into(comment_actions::table) .values(comment_saved_form) - .on_conflict((comment_id, person_id)) + .on_conflict((comment_actions::comment_id, comment_actions::person_id)) .do_update() .set(comment_saved_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unsave( pool: &mut DbPool<'_>, comment_saved_form: &CommentSavedForm, - ) -> Result { - use crate::schema::comment_saved::dsl::comment_saved; + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - comment_saved.find((comment_saved_form.person_id, comment_saved_form.comment_id)), + uplete::new( + comment_actions::table.find((comment_saved_form.person_id, comment_saved_form.comment_id)), ) - .execute(conn) + .set_null(comment_actions::saved) + .get_result(conn) .await } } @@ -216,7 +232,7 @@ mod tests { post::{Post, PostInsertForm}, }, traits::{Crud, Likeable, Saveable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use diesel_ltree::Ltree; use lemmy_utils::error::LemmyResult; @@ -342,8 +358,8 @@ mod tests { format!("0.{}.{}", expected_comment.id, inserted_child_comment.id), inserted_child_comment.path.0, ); - assert_eq!(1, like_removed); - assert_eq!(1, saved_removed); + assert_eq!(uplete::Count::only_updated(1), like_removed); + assert_eq!(uplete::Count::only_deleted(1), saved_removed); assert_eq!(1, num_deleted); Ok(()) diff --git a/crates/db_schema/src/impls/comment_report.rs b/crates/db_schema/src/impls/comment_report.rs index 19c12876f..4c6a1e0d0 100644 --- a/crates/db_schema/src/impls/comment_report.rs +++ b/crates/db_schema/src/impls/comment_report.rs @@ -6,8 +6,9 @@ use crate::{ }, source::comment_report::{CommentReport, CommentReportForm}, traits::Reportable, - utils::{get_conn, naive_now, DbPool}, + utils::{get_conn, DbPool}, }; +use chrono::Utc; use diesel::{ dsl::{insert_into, update}, result::Error, @@ -51,7 +52,7 @@ impl Reportable for CommentReport { .set(( resolved.eq(true), resolver_id.eq(by_resolver_id), - updated.eq(naive_now()), + updated.eq(Utc::now()), )) .execute(conn) .await @@ -67,7 +68,7 @@ impl Reportable for CommentReport { .set(( resolved.eq(true), resolver_id.eq(by_resolver_id), - updated.eq(naive_now()), + updated.eq(Utc::now()), )) .execute(conn) .await @@ -88,7 +89,7 @@ impl Reportable for CommentReport { .set(( resolved.eq(false), resolver_id.eq(by_resolver_id), - updated.eq(naive_now()), + updated.eq(Utc::now()), )) .execute(conn) .await diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 5375bcc3c..3a7ac7dc6 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -1,14 +1,7 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId}, - schema::{ - community, - community_follower, - community_moderator, - community_person_ban, - instance, - post, - }, + schema::{community, community_actions, instance, post}, source::{ actor_language::CommunityLanguage, community::{ @@ -27,8 +20,12 @@ use crate::{ }, traits::{ApubActor, Bannable, Crud, Followable, Joinable}, utils::{ - functions::{coalesce, lower}, + action_query, + find_action, + functions::{coalesce, lower, random}, get_conn, + now, + uplete, DbPool, }, ListingType, @@ -38,6 +35,7 @@ use chrono::{DateTime, Utc}; use diesel::{ deserialize, dsl::{self, exists, insert_into, not}, + expression::SelectableHelper, pg::Pg, result::Error, select, @@ -93,8 +91,19 @@ impl Joinable for CommunityModerator { community_moderator_form: &CommunityModeratorForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_moderator::table) + let community_moderator_form = ( + community_moderator_form, + community_actions::became_moderator.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_moderator_form) + .on_conflict(( + community_actions::person_id, + community_actions::community_id, + )) + .do_update() + .set(community_moderator_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -102,13 +111,14 @@ impl Joinable for CommunityModerator { async fn leave( pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator::table.find(( + uplete::new(community_actions::table.find(( community_moderator_form.person_id, community_moderator_form.community_id, ))) - .execute(conn) + .set_null(community_actions::became_moderator) + .get_result(conn) .await } } @@ -201,7 +211,6 @@ impl Community { type_: &Option, ) -> Result { let conn = &mut get_conn(pool).await?; - sql_function!(fn random() -> Text); let mut query = community::table .filter(not(community::deleted)) @@ -225,26 +234,26 @@ impl CommunityModerator { pub async fn delete_for_community( pool: &mut DbPool<'_>, for_community_id: CommunityId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - community_moderator::table.filter(community_moderator::community_id.eq(for_community_id)), + uplete::new( + community_actions::table.filter(community_actions::community_id.eq(for_community_id)), ) - .execute(conn) + .set_null(community_actions::became_moderator) + .get_result(conn) .await } pub async fn leave_all_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - community_moderator::table.filter(community_moderator::person_id.eq(for_person_id)), - ) - .execute(conn) - .await + uplete::new(community_actions::table.filter(community_actions::person_id.eq(for_person_id))) + .set_null(community_actions::became_moderator) + .get_result(conn) + .await } pub async fn get_person_moderated_communities( @@ -252,9 +261,9 @@ impl CommunityModerator { for_person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table - .filter(community_moderator::person_id.eq(for_person_id)) - .select(community_moderator::community_id) + action_query(community_actions::became_moderator) + .filter(community_actions::person_id.eq(for_person_id)) + .select(community_actions::community_id) .load::(conn) .await } @@ -273,16 +282,17 @@ impl CommunityModerator { persons.push(mod_person_id); persons.dedup(); - let res = community_moderator::table - .filter(community_moderator::community_id.eq(for_community_id)) - .filter(community_moderator::person_id.eq_any(persons)) - .order_by(community_moderator::published) + let res = action_query(community_actions::became_moderator) + .filter(community_actions::community_id.eq(for_community_id)) + .filter(community_actions::person_id.eq_any(persons)) + .order_by(community_actions::became_moderator) + .select(community_actions::person_id) // This does a limit 1 select first - .first::(conn) + .first::(conn) .await?; // If the first result sorted by published is the acting mod - if res.person_id == mod_person_id { + if res == mod_person_id { Ok(()) } else { Err(LemmyErrorType::NotHigherMod)? @@ -298,14 +308,19 @@ impl Bannable for CommunityPersonBan { community_person_ban_form: &CommunityPersonBanForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_person_ban::table) + let community_person_ban_form = ( + community_person_ban_form, + community_actions::received_ban.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_person_ban_form) .on_conflict(( - community_person_ban::community_id, - community_person_ban::person_id, + community_actions::community_id, + community_actions::person_id, )) .do_update() .set(community_person_ban_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -313,20 +328,22 @@ impl Bannable for CommunityPersonBan { async fn unban( pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_person_ban::table.find(( + uplete::new(community_actions::table.find(( community_person_ban_form.person_id, community_person_ban_form.community_id, ))) - .execute(conn) + .set_null(community_actions::received_ban) + .set_null(community_actions::ban_expires) + .get_result(conn) .await } } impl CommunityFollower { - pub fn select_subscribed_type() -> dsl::Nullable { - community_follower::state.nullable() + pub fn select_subscribed_type() -> dsl::Nullable { + community_actions::follow_state.nullable() } /// Check if a remote instance has any followers on local instance. For this it is enough to check @@ -336,9 +353,10 @@ impl CommunityFollower { remote_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(exists(community_follower::table.filter( - community_follower::community_id.eq(remote_community_id), - ))) + select(exists( + action_query(community_actions::followed) + .filter(community_actions::community_id.eq(remote_community_id)), + )) .get_result::(conn) .await? .then_some(()) @@ -352,13 +370,16 @@ impl CommunityFollower { approver_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - diesel::update(community_follower::table.find((follower_id, community_id))) - .set(( - community_follower::state.eq(CommunityFollowerState::Accepted), - community_follower::approver_id.eq(approver_id), - )) - .get_result::(conn) - .await?; + diesel::update(find_action( + community_actions::followed, + (follower_id, community_id), + )) + .set(( + community_actions::follow_state.eq(CommunityFollowerState::Accepted), + community_actions::follow_approver_id.eq(approver_id), + )) + .execute(conn) + .await?; Ok(()) } } @@ -382,14 +403,16 @@ impl Followable for CommunityFollower { type Form = CommunityFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_follower::table) + let form = (form, community_actions::followed.eq(now().nullable())); + insert_into(community_actions::table) .values(form) .on_conflict(( - community_follower::community_id, - community_follower::person_id, + community_actions::community_id, + community_actions::person_id, )) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -399,16 +422,25 @@ impl Followable for CommunityFollower { person_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(community_follower::table.find((person_id, community_id))) - .set(community_follower::state.eq(CommunityFollowerState::Accepted)) - .get_result::(conn) - .await + diesel::update(find_action( + community_actions::follow_state, + (person_id, community_id), + )) + .set(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) + .returning(Self::as_select()) + .get_result::(conn) + .await } - - async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { + async fn unfollow( + pool: &mut DbPool<'_>, + form: &CommunityFollowerForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_follower::table.find((form.person_id, form.community_id))) - .execute(conn) + uplete::new(community_actions::table.find((form.person_id, form.community_id))) + .set_null(community_actions::followed) + .set_null(community_actions::follow_state) + .set_null(community_actions::follow_approver_id) + .get_result(conn) .await } } @@ -483,7 +515,7 @@ mod tests { person::{Person, PersonInsertForm}, }, traits::{Bannable, Crud, Followable, Joinable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, CommunityVisibility, }; use lemmy_utils::error::LemmyResult; @@ -650,9 +682,9 @@ mod tests { assert_eq!(expected_community_follower, inserted_community_follower); assert_eq!(expected_community_moderator, inserted_bobby_moderator); assert_eq!(expected_community_person_ban, inserted_community_person_ban); - assert_eq!(1, ignored_community); - assert_eq!(1, left_community); - assert_eq!(1, unban); + assert_eq!(uplete::Count::only_updated(1), ignored_community); + assert_eq!(uplete::Count::only_updated(1), left_community); + assert_eq!(uplete::Count::only_deleted(1), unban); // assert_eq!(2, loaded_count); assert_eq!(1, num_deleted); diff --git a/crates/db_schema/src/impls/community_block.rs b/crates/db_schema/src/impls/community_block.rs index c78953d27..c520e43e8 100644 --- a/crates/db_schema/src/impls/community_block.rs +++ b/crates/db_schema/src/impls/community_block.rs @@ -1,18 +1,20 @@ use crate::{ newtypes::{CommunityId, PersonId}, - schema::{community, community_block}, + schema::{community, community_actions}, source::{ community::Community, community_block::{CommunityBlock, CommunityBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -25,9 +27,10 @@ impl CommunityBlock { for_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - community_block::table.find((for_person_id, for_community_id)), - ))) + select(not(exists(find_action( + community_actions::blocked, + (for_person_id, for_community_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -39,13 +42,13 @@ impl CommunityBlock { person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_block::table + action_query(community_actions::blocked) .inner_join(community::table) .select(community::all_columns) - .filter(community_block::person_id.eq(person_id)) + .filter(community_actions::person_id.eq(person_id)) .filter(community::deleted.eq(false)) .filter(community::removed.eq(false)) - .order_by(community_block::published) + .order_by(community_actions::blocked) .load::(conn) .await } @@ -56,24 +59,33 @@ impl Blockable for CommunityBlock { type Form = CommunityBlockForm; async fn block(pool: &mut DbPool<'_>, community_block_form: &Self::Form) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_block::table) + let community_block_form = ( + community_block_form, + community_actions::blocked.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_block_form) - .on_conflict((community_block::person_id, community_block::community_id)) + .on_conflict(( + community_actions::person_id, + community_actions::community_id, + )) .do_update() .set(community_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unblock( pool: &mut DbPool<'_>, community_block_form: &Self::Form, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_block::table.find(( + uplete::new(community_actions::table.find(( community_block_form.person_id, community_block_form.community_id, ))) - .execute(conn) + .set_null(community_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index 099e0b231..41ced26f7 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -1,60 +1,51 @@ use crate::{ - schema::federation_allowlist, + newtypes::InstanceId, + schema::{admin_allow_instance, federation_allowlist}, source::{ federation_allowlist::{FederationAllowList, FederationAllowListForm}, - instance::Instance, + mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, }, utils::{get_conn, DbPool}, }; -use diesel::{dsl::insert_into, result::Error}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; -impl FederationAllowList { - pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option>) -> Result<(), Error> { +impl AdminAllowInstance { + pub async fn insert(pool: &mut DbPool<'_>, form: &AdminAllowInstanceForm) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - if let Some(list) = list_opt { - Self::clear(conn).await?; - - for domain in list { - // Upsert all of these as instances - let instance = Instance::read_or_create(&mut conn.into(), domain).await?; - - let form = FederationAllowListForm { - instance_id: instance.id, - updated: None, - }; - insert_into(federation_allowlist::table) - .values(form) - .get_result::(conn) - .await?; - } - Ok(()) - } else { - Ok(()) - } - }) as _ - }) - .await - } - - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(federation_allowlist::table) + insert_into(admin_allow_instance::table) + .values(form) .execute(conn) - .await + .await?; + + Ok(()) } } + +impl FederationAllowList { + pub async fn allow(pool: &mut DbPool<'_>, form: &FederationAllowListForm) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + insert_into(federation_allowlist::table) + .values(form) + .execute(conn) + .await?; + Ok(()) + } + pub async fn unallow(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result<(), Error> { + use federation_allowlist::dsl::instance_id; + let conn = &mut get_conn(pool).await?; + delete(federation_allowlist::table.filter(instance_id.eq(instance_id_))) + .execute(conn) + .await?; + Ok(()) + } +} + #[cfg(test)] mod tests { - use crate::{ - source::{federation_allowlist::FederationAllowList, instance::Instance}, - utils::build_db_pool_for_tests, - }; - use diesel::result::Error; + use super::*; + use crate::{source::instance::Instance, utils::build_db_pool_for_tests}; use pretty_assertions::assert_eq; use serial_test::serial; @@ -63,31 +54,33 @@ mod tests { async fn test_allowlist_insert_and_clear() -> Result<(), Error> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let domains = vec![ - "tld1.xyz".to_string(), - "tld2.xyz".to_string(), - "tld3.xyz".to_string(), + let instances = vec![ + Instance::read_or_create(pool, "tld1.xyz".to_string()).await?, + Instance::read_or_create(pool, "tld2.xyz".to_string()).await?, + Instance::read_or_create(pool, "tld3.xyz".to_string()).await?, ]; + let forms: Vec<_> = instances + .iter() + .map(|i| FederationAllowListForm { + instance_id: i.id, + updated: None, + }) + .collect(); - let allowed = Some(domains.clone()); - - FederationAllowList::replace(pool, allowed).await?; + for f in &forms { + FederationAllowList::allow(pool, f).await?; + } let allows = Instance::allowlist(pool).await?; - let allows_domains = allows - .iter() - .map(|i| i.domain.clone()) - .collect::>(); assert_eq!(3, allows.len()); - assert_eq!(domains, allows_domains); + assert_eq!(instances, allows); - // Now test clearing them via Some(empty vec) - let clear_allows = Some(Vec::new()); - - FederationAllowList::replace(pool, clear_allows).await?; + // Now test clearing them + for f in forms { + FederationAllowList::unallow(pool, f.instance_id).await?; + } let allows = Instance::allowlist(pool).await?; - assert_eq!(0, allows.len()); Instance::delete_all(pool).await?; diff --git a/crates/db_schema/src/impls/federation_blocklist.rs b/crates/db_schema/src/impls/federation_blocklist.rs index 2a6e0671d..4a42e81b6 100644 --- a/crates/db_schema/src/impls/federation_blocklist.rs +++ b/crates/db_schema/src/impls/federation_blocklist.rs @@ -1,49 +1,42 @@ use crate::{ - schema::federation_blocklist, + newtypes::InstanceId, + schema::{admin_block_instance, federation_blocklist}, source::{ federation_blocklist::{FederationBlockList, FederationBlockListForm}, - instance::Instance, + mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, }, utils::{get_conn, DbPool}, }; -use diesel::{dsl::insert_into, result::Error}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; -impl FederationBlockList { - pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option>) -> Result<(), Error> { +impl AdminBlockInstance { + pub async fn insert(pool: &mut DbPool<'_>, form: &AdminBlockInstanceForm) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - if let Some(list) = list_opt { - Self::clear(conn).await?; - - for domain in list { - // Upsert all of these as instances - let instance = Instance::read_or_create(&mut conn.into(), domain).await?; - - let form = FederationBlockListForm { - instance_id: instance.id, - updated: None, - }; - insert_into(federation_blocklist::table) - .values(form) - .get_result::(conn) - .await?; - } - Ok(()) - } else { - Ok(()) - } - }) as _ - }) - .await - } - - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(federation_blocklist::table) + insert_into(admin_block_instance::table) + .values(form) .execute(conn) - .await + .await?; + + Ok(()) + } +} + +impl FederationBlockList { + pub async fn block(pool: &mut DbPool<'_>, form: &FederationBlockListForm) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + insert_into(federation_blocklist::table) + .values(form) + .execute(conn) + .await?; + Ok(()) + } + pub async fn unblock(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result<(), Error> { + use federation_blocklist::dsl::instance_id; + let conn = &mut get_conn(pool).await?; + delete(federation_blocklist::table.filter(instance_id.eq(instance_id_))) + .execute(conn) + .await?; + Ok(()) } } diff --git a/crates/db_schema/src/impls/instance.rs b/crates/db_schema/src/impls/instance.rs index 6c72b5e18..d638786fe 100644 --- a/crates/db_schema/src/impls/instance.rs +++ b/crates/db_schema/src/impls/instance.rs @@ -16,11 +16,11 @@ use crate::{ utils::{ functions::{coalesce, lower}, get_conn, - naive_now, now, DbPool, }, }; +use chrono::Utc; use diesel::{ dsl::{count_star, insert_into}, result::Error, @@ -52,7 +52,7 @@ impl Instance { None => { // Instance not in database yet, insert it let form = InstanceForm { - updated: Some(naive_now()), + updated: Some(Utc::now()), ..InstanceForm::new(domain_) }; insert_into(instance::table) diff --git a/crates/db_schema/src/impls/instance_block.rs b/crates/db_schema/src/impls/instance_block.rs index 1b70f0e08..1722e8318 100644 --- a/crates/db_schema/src/impls/instance_block.rs +++ b/crates/db_schema/src/impls/instance_block.rs @@ -1,18 +1,20 @@ use crate::{ newtypes::{InstanceId, PersonId}, - schema::{instance, instance_block}, + schema::{instance, instance_actions}, source::{ instance::Instance, instance_block::{InstanceBlock, InstanceBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -25,9 +27,10 @@ impl InstanceBlock { for_instance_id: InstanceId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - instance_block::table.find((for_person_id, for_instance_id)), - ))) + select(not(exists(find_action( + instance_actions::blocked, + (for_person_id, for_instance_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -39,11 +42,11 @@ impl InstanceBlock { person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - instance_block::table + action_query(instance_actions::blocked) .inner_join(instance::table) .select(instance::all_columns) - .filter(instance_block::person_id.eq(person_id)) - .order_by(instance_block::published) + .filter(instance_actions::person_id.eq(person_id)) + .order_by(instance_actions::blocked) .load::(conn) .await } @@ -54,24 +57,30 @@ impl Blockable for InstanceBlock { type Form = InstanceBlockForm; async fn block(pool: &mut DbPool<'_>, instance_block_form: &Self::Form) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(instance_block::table) + let instance_block_form = ( + instance_block_form, + instance_actions::blocked.eq(now().nullable()), + ); + insert_into(instance_actions::table) .values(instance_block_form) - .on_conflict((instance_block::person_id, instance_block::instance_id)) + .on_conflict((instance_actions::person_id, instance_actions::instance_id)) .do_update() .set(instance_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unblock( pool: &mut DbPool<'_>, instance_block_form: &Self::Form, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(instance_block::table.find(( + uplete::new(instance_actions::table.find(( instance_block_form.person_id, instance_block_form.instance_id, ))) - .execute(conn) + .set_null(instance_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/local_site.rs b/crates/db_schema/src/impls/local_site.rs index 926814c48..bdbe4ac6c 100644 --- a/crates/db_schema/src/impls/local_site.rs +++ b/crates/db_schema/src/impls/local_site.rs @@ -5,8 +5,7 @@ use crate::{ }; use diesel::{dsl::insert_into, result::Error}; use diesel_async::RunQueryDsl; -use lemmy_utils::{error::LemmyResult, CACHE_DURATION_API}; -use moka::future::Cache; +use lemmy_utils::{build_cache, error::LemmyResult, CacheLock}; use std::sync::LazyLock; impl LocalSite { @@ -18,12 +17,7 @@ impl LocalSite { .await } pub async fn read(pool: &mut DbPool<'_>) -> LemmyResult { - static CACHE: LazyLock> = LazyLock::new(|| { - Cache::builder() - .max_capacity(1) - .time_to_live(CACHE_DURATION_API) - .build() - }); + static CACHE: CacheLock = LazyLock::new(build_cache); Ok( CACHE .try_get_with((), async { diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 69a5ef314..6c50e1c94 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId}, - schema::{community, community_moderator, local_user, person, registration_application}, + schema::{community, community_actions, local_user, person, registration_application}, source::{ actor_language::LocalUserLanguage, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, @@ -8,6 +8,7 @@ use crate::{ site::Site, }, utils::{ + action_query, functions::{coalesce, lower}, get_conn, now, @@ -25,19 +26,19 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; impl LocalUser { pub async fn create( pool: &mut DbPool<'_>, form: &LocalUserInsertForm, languages: Vec, - ) -> Result { + ) -> LemmyResult { let conn = &mut get_conn(pool).await?; let mut form_with_encrypted_password = form.clone(); if let Some(password_encrypted) = &form.password_encrypted { - let password_hash = hash(password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); + let password_hash = hash(password_encrypted, DEFAULT_COST)?; form_with_encrypted_password.password_encrypted = Some(password_hash); } @@ -83,14 +84,15 @@ impl LocalUser { pool: &mut DbPool<'_>, local_user_id: LocalUserId, new_password: &str, - ) -> Result { + ) -> LemmyResult { let conn = &mut get_conn(pool).await?; - let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password"); + let password_hash = hash(new_password, DEFAULT_COST)?; diesel::update(local_user::table.find(local_user_id)) .set((local_user::password_encrypted.eq(password_hash),)) .get_result::(conn) .await + .with_lemmy_type(LemmyErrorType::CouldntUpdateUser) } pub async fn set_all_users_email_verified(pool: &mut DbPool<'_>) -> Result, Error> { @@ -155,55 +157,54 @@ impl LocalUser { ) -> Result { use crate::schema::{ comment, - comment_saved, + comment_actions, community, - community_block, - community_follower, + community_actions, instance, - instance_block, - person_block, + instance_actions, + person_actions, post, - post_saved, + post_actions, }; let conn = &mut get_conn(pool).await?; - let followed_communities = community_follower::dsl::community_follower - .filter(community_follower::person_id.eq(person_id_)) - .inner_join(community::table.on(community_follower::community_id.eq(community::id))) - .select(community::actor_id) - .get_results(conn) - .await?; - - let saved_posts = post_saved::dsl::post_saved - .filter(post_saved::person_id.eq(person_id_)) - .inner_join(post::table.on(post_saved::post_id.eq(post::id))) - .select(post::ap_id) - .get_results(conn) - .await?; - - let saved_comments = comment_saved::dsl::comment_saved - .filter(comment_saved::person_id.eq(person_id_)) - .inner_join(comment::table.on(comment_saved::comment_id.eq(comment::id))) - .select(comment::ap_id) - .get_results(conn) - .await?; - - let blocked_communities = community_block::dsl::community_block - .filter(community_block::person_id.eq(person_id_)) + let followed_communities = action_query(community_actions::followed) + .filter(community_actions::person_id.eq(person_id_)) .inner_join(community::table) .select(community::actor_id) .get_results(conn) .await?; - let blocked_users = person_block::dsl::person_block - .filter(person_block::person_id.eq(person_id_)) - .inner_join(person::table.on(person_block::target_id.eq(person::id))) + let saved_posts = action_query(post_actions::saved) + .filter(post_actions::person_id.eq(person_id_)) + .inner_join(post::table) + .select(post::ap_id) + .get_results(conn) + .await?; + + let saved_comments = action_query(comment_actions::saved) + .filter(comment_actions::person_id.eq(person_id_)) + .inner_join(comment::table) + .select(comment::ap_id) + .get_results(conn) + .await?; + + let blocked_communities = action_query(community_actions::blocked) + .filter(community_actions::person_id.eq(person_id_)) + .inner_join(community::table) + .select(community::actor_id) + .get_results(conn) + .await?; + + let blocked_users = action_query(person_actions::blocked) + .filter(person_actions::person_id.eq(person_id_)) + .inner_join(person::table.on(person_actions::target_id.eq(person::id))) .select(person::actor_id) .get_results(conn) .await?; - let blocked_instances = instance_block::dsl::instance_block - .filter(instance_block::person_id.eq(person_id_)) + let blocked_instances = action_query(instance_actions::blocked) + .filter(instance_actions::person_id.eq(person_id_)) .inner_join(instance::table) .select(instance::domain) .get_results(conn) @@ -270,11 +271,11 @@ impl LocalUser { .order_by(local_user::id) .select(local_user::person_id); - let mods = community_moderator::table - .filter(community_moderator::community_id.eq(for_community_id)) - .filter(community_moderator::person_id.eq_any(&persons)) - .order_by(community_moderator::published) - .select(community_moderator::person_id); + let mods = action_query(community_actions::became_moderator) + .filter(community_actions::community_id.eq(for_community_id)) + .filter(community_actions::person_id.eq_any(&persons)) + .order_by(community_actions::became_moderator) + .select(community_actions::person_id); let res = admins.union_all(mods).get_results::(conn).await?; let first_person = res.as_slice().first().ok_or(LemmyErrorType::NotHigherMod)?; diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index f18c05a91..7f3b231dd 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -22,7 +22,7 @@ pub mod local_site_url_blocklist; pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; -pub mod moderator; +pub mod mod_log; pub mod oauth_account; pub mod oauth_provider; pub mod password_reset_request; diff --git a/crates/db_schema/src/impls/mod_log/admin.rs b/crates/db_schema/src/impls/mod_log/admin.rs new file mode 100644 index 000000000..c1b2bf69f --- /dev/null +++ b/crates/db_schema/src/impls/mod_log/admin.rs @@ -0,0 +1,132 @@ +use crate::{ + source::mod_log::admin::{ + AdminPurgeComment, + AdminPurgeCommentForm, + AdminPurgeCommunity, + AdminPurgeCommunityForm, + AdminPurgePerson, + AdminPurgePersonForm, + AdminPurgePost, + AdminPurgePostForm, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for AdminPurgePerson { + type InsertForm = AdminPurgePersonForm; + type UpdateForm = AdminPurgePersonForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_person::dsl::admin_purge_person; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_person) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_person::dsl::admin_purge_person; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_person.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgeCommunity { + type InsertForm = AdminPurgeCommunityForm; + type UpdateForm = AdminPurgeCommunityForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_community::dsl::admin_purge_community; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_community) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_community::dsl::admin_purge_community; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_community.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgePost { + type InsertForm = AdminPurgePostForm; + type UpdateForm = AdminPurgePostForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_post::dsl::admin_purge_post; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_post) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_post::dsl::admin_purge_post; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_post.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgeComment { + type InsertForm = AdminPurgeCommentForm; + type UpdateForm = AdminPurgeCommentForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_comment::dsl::admin_purge_comment; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_comment) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_comment::dsl::admin_purge_comment; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_comment.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/mod_log/mod.rs b/crates/db_schema/src/impls/mod_log/mod.rs new file mode 100644 index 000000000..54341c69a --- /dev/null +++ b/crates/db_schema/src/impls/mod_log/mod.rs @@ -0,0 +1,2 @@ +pub mod admin; +pub mod moderator; diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/mod_log/moderator.rs similarity index 82% rename from crates/db_schema/src/impls/moderator.rs rename to crates/db_schema/src/impls/mod_log/moderator.rs index 8deb56258..37b66480d 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/mod_log/moderator.rs @@ -1,13 +1,5 @@ use crate::{ - source::moderator::{ - AdminPurgeComment, - AdminPurgeCommentForm, - AdminPurgeCommunity, - AdminPurgeCommunityForm, - AdminPurgePerson, - AdminPurgePersonForm, - AdminPurgePost, - AdminPurgePostForm, + source::mod_log::moderator::{ ModAdd, ModAddCommunity, ModAddCommunityForm, @@ -376,157 +368,20 @@ impl Crud for ModAdd { } } -#[async_trait] -impl Crud for AdminPurgePerson { - type InsertForm = AdminPurgePersonForm; - type UpdateForm = AdminPurgePersonForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_person::dsl::admin_purge_person; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_person) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_person::dsl::admin_purge_person; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_person.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgeCommunity { - type InsertForm = AdminPurgeCommunityForm; - type UpdateForm = AdminPurgeCommunityForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_community::dsl::admin_purge_community; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_community) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_community::dsl::admin_purge_community; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_community.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgePost { - type InsertForm = AdminPurgePostForm; - type UpdateForm = AdminPurgePostForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_post::dsl::admin_purge_post; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_post) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_post::dsl::admin_purge_post; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_post.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgeComment { - type InsertForm = AdminPurgeCommentForm; - type UpdateForm = AdminPurgeCommentForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_comment::dsl::admin_purge_comment; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_comment) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_comment::dsl::admin_purge_comment; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_comment.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - #[cfg(test)] mod tests { + use super::*; use crate::{ source::{ comment::{Comment, CommentInsertForm}, community::{Community, CommunityInsertForm}, instance::Instance, - moderator::{ - ModAdd, - ModAddCommunity, - ModAddCommunityForm, - ModAddForm, - ModBan, - ModBanForm, - ModBanFromCommunity, - ModBanFromCommunityForm, - ModFeaturePost, - ModFeaturePostForm, - ModLockPost, - ModLockPostForm, - ModRemoveComment, - ModRemoveCommentForm, - ModRemoveCommunity, - ModRemoveCommunityForm, - ModRemovePost, - ModRemovePostForm, - }, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm}, }, - traits::Crud, utils::build_db_pool_for_tests, }; - use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index 85ab20d6a..fb8c96f04 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -1,7 +1,7 @@ use crate::{ diesel::OptionalExtension, newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, - schema::{comment, community, instance, local_user, person, person_follower, post}, + schema::{comment, community, instance, local_user, person, person_actions, post}, source::person::{ Person, PersonFollower, @@ -10,14 +10,17 @@ use crate::{ PersonUpdateForm, }, traits::{ApubActor, Crud, Followable}, - utils::{functions::lower, get_conn, naive_now, DbPool}, + utils::{action_query, functions::lower, get_conn, now, uplete, DbPool}, }; +use chrono::Utc; use diesel::{ dsl::{insert_into, not}, + expression::SelectableHelper, result::Error, CombineDsl, ExpressionMethods, JoinOnDsl, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -91,7 +94,7 @@ impl Person { person::bio.eq::>(None), person::matrix_user_id.eq::>(None), person::deleted.eq(true), - person::updated.eq(naive_now()), + person::updated.eq(Utc::now()), )) .get_result::(conn) .await @@ -197,11 +200,13 @@ impl Followable for PersonFollower { type Form = PersonFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_follower::table) + let form = (form, person_actions::followed.eq(now().nullable())); + insert_into(person_actions::table) .values(form) - .on_conflict((person_follower::follower_id, person_follower::person_id)) + .on_conflict((person_actions::person_id, person_actions::target_id)) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -211,10 +216,15 @@ impl Followable for PersonFollower { Err(Error::NotFound) } - async fn unfollow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { + async fn unfollow( + pool: &mut DbPool<'_>, + form: &PersonFollowerForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(person_follower::table.find((form.follower_id, form.person_id))) - .execute(conn) + uplete::new(person_actions::table.find((form.follower_id, form.person_id))) + .set_null(person_actions::followed) + .set_null(person_actions::follow_pending) + .get_result(conn) .await } } @@ -225,9 +235,9 @@ impl PersonFollower { for_person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - person_follower::table - .inner_join(person::table.on(person_follower::follower_id.eq(person::id))) - .filter(person_follower::person_id.eq(for_person_id)) + action_query(person_actions::followed) + .inner_join(person::table.on(person_actions::person_id.eq(person::id))) + .filter(person_actions::target_id.eq(for_person_id)) .select(person::all_columns) .load(conn) .await @@ -243,7 +253,7 @@ mod tests { person::{Person, PersonFollower, PersonFollowerForm, PersonInsertForm, PersonUpdateForm}, }, traits::{Crud, Followable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; @@ -329,7 +339,7 @@ mod tests { assert_eq!(vec![person_2], followers); let unfollow = PersonFollower::unfollow(pool, &follow_form).await?; - assert_eq!(1, unfollow); + assert_eq!(uplete::Count::only_deleted(1), unfollow); Ok(()) } diff --git a/crates/db_schema/src/impls/person_block.rs b/crates/db_schema/src/impls/person_block.rs index 44c83b3f8..363a2d3d1 100644 --- a/crates/db_schema/src/impls/person_block.rs +++ b/crates/db_schema/src/impls/person_block.rs @@ -1,19 +1,21 @@ use crate::{ newtypes::PersonId, - schema::{person, person_block}, + schema::{person, person_actions}, source::{ person::Person, person_block::{PersonBlock, PersonBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, JoinOnDsl, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -26,9 +28,10 @@ impl PersonBlock { for_recipient_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - person_block::table.find((for_person_id, for_recipient_id)), - ))) + select(not(exists(find_action( + person_actions::blocked, + (for_person_id, for_recipient_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -42,15 +45,15 @@ impl PersonBlock { let conn = &mut get_conn(pool).await?; let target_person_alias = diesel::alias!(person as person1); - person_block::table - .inner_join(person::table.on(person_block::person_id.eq(person::id))) + action_query(person_actions::blocked) + .inner_join(person::table.on(person_actions::person_id.eq(person::id))) .inner_join( - target_person_alias.on(person_block::target_id.eq(target_person_alias.field(person::id))), + target_person_alias.on(person_actions::target_id.eq(target_person_alias.field(person::id))), ) .select(target_person_alias.fields(person::all_columns)) - .filter(person_block::person_id.eq(person_id)) + .filter(person_actions::person_id.eq(person_id)) .filter(target_person_alias.field(person::deleted).eq(false)) - .order_by(person_block::published) + .order_by(person_actions::blocked) .load::(conn) .await } @@ -64,20 +67,29 @@ impl Blockable for PersonBlock { person_block_form: &PersonBlockForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_block::table) + let person_block_form = ( + person_block_form, + person_actions::blocked.eq(now().nullable()), + ); + insert_into(person_actions::table) .values(person_block_form) - .on_conflict((person_block::person_id, person_block::target_id)) + .on_conflict((person_actions::person_id, person_actions::target_id)) .do_update() .set(person_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } - async fn unblock(pool: &mut DbPool<'_>, person_block_form: &Self::Form) -> Result { + async fn unblock( + pool: &mut DbPool<'_>, + person_block_form: &Self::Form, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - person_block::table.find((person_block_form.person_id, person_block_form.target_id)), + uplete::new( + person_actions::table.find((person_block_form.person_id, person_block_form.target_id)), ) - .execute(conn) + .set_null(person_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index bd99344b9..064e21b5f 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -1,7 +1,7 @@ use crate::{ - diesel::{BoolExpressionMethods, OptionalExtension}, + diesel::{BoolExpressionMethods, NullableExpressionMethods, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId, PostId}, - schema::{community, person, post, post_hide, post_like, post_read, post_saved}, + schema::{community, person, post, post_actions}, source::post::{ Post, PostHide, @@ -19,8 +19,8 @@ use crate::{ utils::{ functions::coalesce, get_conn, - naive_now, now, + uplete, DbPool, DELETED_REPLACEMENT_TEXT, FETCH_LIMIT_MAX, @@ -32,6 +32,7 @@ use ::url::Url; use chrono::{DateTime, Utc}; use diesel::{ dsl::{count, insert_into, not}, + expression::SelectableHelper, result::Error, DecoratableTarget, ExpressionMethods, @@ -39,7 +40,7 @@ use diesel::{ TextExpressionMethods, }; use diesel_async::RunQueryDsl; -use std::collections::HashSet; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[async_trait] impl Crud for Post { @@ -114,9 +115,7 @@ impl Post { .filter(post::local.eq(true)) .filter(post::deleted.eq(false)) .filter(post::removed.eq(false)) - .filter( - post::published.ge(Utc::now().naive_utc() - SITEMAP_DAYS.expect("TimeDelta out of bounds")), - ) + .filter(post::published.ge(Utc::now().naive_utc() - SITEMAP_DAYS)) .order(post::published.desc()) .limit(SITEMAP_LIMIT) .load::<(DbUrl, chrono::DateTime)>(conn) @@ -135,7 +134,7 @@ impl Post { post::url.eq(Option::<&str>::None), post::body.eq(DELETED_REPLACEMENT_TEXT), post::deleted.eq(true), - post::updated.eq(naive_now()), + post::updated.eq(Utc::now()), )) .get_results::(conn) .await @@ -157,7 +156,7 @@ impl Post { } update - .set((post::removed.eq(removed), post::updated.eq(naive_now()))) + .set((post::removed.eq(removed), post::updated.eq(Utc::now()))) .get_results::(conn) .await } @@ -278,11 +277,12 @@ impl Likeable for PostLike { type IdType = PostId; async fn like(pool: &mut DbPool<'_>, post_like_form: &PostLikeForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post_like::table) + insert_into(post_actions::table) .values(post_like_form) - .on_conflict((post_like::post_id, post_like::person_id)) + .on_conflict((post_actions::post_id, post_actions::person_id)) .do_update() .set(post_like_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -290,10 +290,12 @@ impl Likeable for PostLike { pool: &mut DbPool<'_>, person_id: PersonId, post_id: PostId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(post_like::table.find((person_id, post_id))) - .execute(conn) + uplete::new(post_actions::table.find((person_id, post_id))) + .set_null(post_actions::like_score) + .set_null(post_actions::liked) + .get_result(conn) .await } } @@ -303,18 +305,23 @@ impl Saveable for PostSaved { type Form = PostSavedForm; async fn save(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post_saved::table) + insert_into(post_actions::table) .values(post_saved_form) - .on_conflict((post_saved::post_id, post_saved::person_id)) + .on_conflict((post_actions::post_id, post_actions::person_id)) .do_update() .set(post_saved_form) + .returning(Self::as_select()) .get_result::(conn) .await } - async fn unsave(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { + async fn unsave( + pool: &mut DbPool<'_>, + post_saved_form: &PostSavedForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(post_saved::table.find((post_saved_form.person_id, post_saved_form.post_id))) - .execute(conn) + uplete::new(post_actions::table.find((post_saved_form.person_id, post_saved_form.post_id))) + .set_null(post_actions::saved) + .get_result(conn) .await } } @@ -322,71 +329,82 @@ impl Saveable for PostSaved { impl PostRead { pub async fn mark_as_read( pool: &mut DbPool<'_>, - post_ids: HashSet, - person_id: PersonId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - - let forms = post_ids - .into_iter() - .map(|post_id| PostReadForm { post_id, person_id }) - .collect::>(); - insert_into(post_read::table) - .values(forms) - .on_conflict_do_nothing() - .execute(conn) - .await + post_read_form: &PostReadForm, + ) -> LemmyResult { + Self::mark_many_as_read(pool, &[post_read_form.post_id], post_read_form.person_id).await } pub async fn mark_as_unread( pool: &mut DbPool<'_>, - post_id_: HashSet, - person_id_: PersonId, - ) -> Result { + post_read_form: &PostReadForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - post_read::table - .filter(post_read::post_id.eq_any(post_id_)) - .filter(post_read::person_id.eq(person_id_)), + uplete::new( + post_actions::table + .filter(post_actions::post_id.eq(post_read_form.post_id)) + .filter(post_actions::person_id.eq(post_read_form.person_id)), ) - .execute(conn) + .set_null(post_actions::read) + .get_result(conn) .await } + + pub async fn mark_many_as_read( + pool: &mut DbPool<'_>, + post_ids: &[PostId], + person_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + + let forms = post_ids + .iter() + .map(|post_id| (PostReadForm::new(*post_id, person_id))) + .collect::>(); + + insert_into(post_actions::table) + .values(forms) + .on_conflict((post_actions::person_id, post_actions::post_id)) + .do_update() + .set(post_actions::read.eq(now().nullable())) + .execute(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead) + } } impl PostHide { pub async fn hide( pool: &mut DbPool<'_>, - post_ids: HashSet, + post_id: PostId, person_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - let forms = post_ids - .into_iter() - .map(|post_id| PostHideForm { post_id, person_id }) - .collect::>(); - insert_into(post_hide::table) - .values(forms) - .on_conflict_do_nothing() + let form = &PostHideForm::new(post_id, person_id); + insert_into(post_actions::table) + .values(form) + .on_conflict((post_actions::person_id, post_actions::post_id)) + .do_update() + .set(form) .execute(conn) .await } pub async fn unhide( pool: &mut DbPool<'_>, - post_id_: HashSet, + post_id_: PostId, person_id_: PersonId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - post_hide::table - .filter(post_hide::post_id.eq_any(post_id_)) - .filter(post_hide::person_id.eq(person_id_)), + uplete::new( + post_actions::table + .filter(post_actions::post_id.eq(post_id_)) + .filter(post_actions::person_id.eq(person_id_)), ) - .execute(conn) + .set_null(post_actions::hidden) + .get_result(conn) .await } } @@ -405,19 +423,19 @@ mod tests { PostLike, PostLikeForm, PostRead, + PostReadForm, PostSaved, PostSavedForm, PostUpdateForm, }, }, traits::{Crud, Likeable, Saveable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use chrono::DateTime; use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; - use std::collections::HashSet; use url::Url; #[tokio::test] @@ -489,11 +507,7 @@ mod tests { }; // Post Like - let post_like_form = PostLikeForm { - post_id: inserted_post.id, - person_id: inserted_person.id, - score: 1, - }; + let post_like_form = PostLikeForm::new(inserted_post.id, inserted_person.id, 1); let inserted_post_like = PostLike::like(pool, &post_like_form).await?; @@ -505,10 +519,7 @@ mod tests { }; // Post Save - let post_saved_form = PostSavedForm { - post_id: inserted_post.id, - person_id: inserted_person.id, - }; + let post_saved_form = PostSavedForm::new(inserted_post.id, inserted_person.id); let inserted_post_saved = PostSaved::save(pool, &post_saved_form).await?; @@ -518,14 +529,11 @@ mod tests { published: inserted_post_saved.published, }; - // Post Read - let marked_as_read = PostRead::mark_as_read( - pool, - HashSet::from([inserted_post.id, inserted_post2.id]), - inserted_person.id, - ) - .await?; - assert_eq!(2, marked_as_read); + // Mark 2 posts as read + let post_read_form_1 = PostReadForm::new(inserted_post.id, inserted_person.id); + PostRead::mark_as_read(pool, &post_read_form_1).await?; + let post_read_form_2 = PostReadForm::new(inserted_post2.id, inserted_person.id); + PostRead::mark_as_read(pool, &post_read_form_2).await?; let read_post = Post::read(pool, inserted_post.id).await?; @@ -540,20 +548,22 @@ mod tests { assert_eq!(1, scheduled_post_count); let like_removed = PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; - assert_eq!(1, like_removed); + assert_eq!(uplete::Count::only_updated(1), like_removed); let saved_removed = PostSaved::unsave(pool, &post_saved_form).await?; - assert_eq!(1, saved_removed); - let read_removed = PostRead::mark_as_unread( - pool, - HashSet::from([inserted_post.id, inserted_post2.id]), - inserted_person.id, - ) - .await?; - assert_eq!(2, read_removed); + assert_eq!(uplete::Count::only_updated(1), saved_removed); + + let read_remove_form_1 = PostReadForm::new(inserted_post.id, inserted_person.id); + let read_removed_1 = PostRead::mark_as_unread(pool, &read_remove_form_1).await?; + assert_eq!(uplete::Count::only_deleted(1), read_removed_1); + + let read_remove_form_2 = PostReadForm::new(inserted_post2.id, inserted_person.id); + let read_removed_2 = PostRead::mark_as_unread(pool, &read_remove_form_2).await?; + assert_eq!(uplete::Count::only_deleted(1), read_removed_2); let num_deleted = Post::delete(pool, inserted_post.id).await? + Post::delete(pool, inserted_post2.id).await? + Post::delete(pool, inserted_scheduled_post.id).await?; + assert_eq!(3, num_deleted); Community::delete(pool, inserted_community.id).await?; Person::delete(pool, inserted_person.id).await?; diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index e7d27aee9..90ac030c1 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -6,8 +6,9 @@ use crate::{ }, source::post_report::{PostReport, PostReportForm}, traits::Reportable, - utils::{get_conn, naive_now, DbPool}, + utils::{get_conn, DbPool}, }; +use chrono::Utc; use diesel::{ dsl::{insert_into, update}, result::Error, @@ -40,7 +41,7 @@ impl Reportable for PostReport { .set(( resolved.eq(true), resolver_id.eq(by_resolver_id), - updated.eq(naive_now()), + updated.eq(Utc::now()), )) .execute(conn) .await @@ -56,7 +57,7 @@ impl Reportable for PostReport { .set(( resolved.eq(true), resolver_id.eq(by_resolver_id), - updated.eq(naive_now()), + updated.eq(Utc::now()), )) .execute(conn) .await @@ -72,7 +73,7 @@ impl Reportable for PostReport { .set(( resolved.eq(false), resolver_id.eq(by_resolver_id), - updated.eq(naive_now()), + updated.eq(Utc::now()), )) .execute(conn) .await diff --git a/crates/db_schema/src/impls/private_message_report.rs b/crates/db_schema/src/impls/private_message_report.rs index 0d5876659..0a83bf637 100644 --- a/crates/db_schema/src/impls/private_message_report.rs +++ b/crates/db_schema/src/impls/private_message_report.rs @@ -3,8 +3,9 @@ use crate::{ schema::private_message_report::dsl::{private_message_report, resolved, resolver_id, updated}, source::private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, traits::Reportable, - utils::{get_conn, naive_now, DbPool}, + utils::{get_conn, DbPool}, }; +use chrono::Utc; use diesel::{ dsl::{insert_into, update}, result::Error, @@ -40,7 +41,7 @@ impl Reportable for PrivateMessageReport { .set(( resolved.eq(true), resolver_id.eq(by_resolver_id), - updated.eq(naive_now()), + updated.eq(Utc::now()), )) .execute(conn) .await @@ -65,7 +66,7 @@ impl Reportable for PrivateMessageReport { .set(( resolved.eq(false), resolver_id.eq(by_resolver_id), - updated.eq(naive_now()), + updated.eq(Utc::now()), )) .execute(conn) .await diff --git a/crates/db_schema/src/impls/site.rs b/crates/db_schema/src/impls/site.rs index e993639fa..7ab13b8e2 100644 --- a/crates/db_schema/src/impls/site.rs +++ b/crates/db_schema/src/impls/site.rs @@ -10,7 +10,7 @@ use crate::{ }; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl}; use diesel_async::RunQueryDsl; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use url::Url; #[async_trait] @@ -65,13 +65,13 @@ impl Site { pub async fn read_from_instance_id( pool: &mut DbPool<'_>, _instance_id: InstanceId, - ) -> Result, Error> { + ) -> LemmyResult { let conn = &mut get_conn(pool).await?; site::table .filter(site::instance_id.eq(_instance_id)) .first(conn) .await - .optional() + .with_lemmy_type(LemmyErrorType::NotFound) } pub async fn read_from_apub_id( pool: &mut DbPool<'_>, diff --git a/crates/db_schema/src/impls/tagline.rs b/crates/db_schema/src/impls/tagline.rs index aa5841020..ed9f82538 100644 --- a/crates/db_schema/src/impls/tagline.rs +++ b/crates/db_schema/src/impls/tagline.rs @@ -3,7 +3,7 @@ use crate::{ schema::tagline::dsl::{published, tagline}, source::tagline::{Tagline, TaglineInsertForm, TaglineUpdateForm}, traits::Crud, - utils::{get_conn, limit_and_offset, DbPool}, + utils::{functions::random, get_conn, limit_and_offset, DbPool}, }; use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; @@ -53,7 +53,6 @@ impl Tagline { pub async fn get_random(pool: &mut DbPool<'_>) -> Result { let conn = &mut get_conn(pool).await?; - sql_function!(fn random() -> Text); tagline.order(random()).limit(1).first::(conn).await } } diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 0397c939a..ad6e93619 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -30,11 +30,11 @@ pub mod sensitive; pub mod schema; #[cfg(feature = "full")] pub mod aliases { - use crate::schema::{community_moderator, person}; + use crate::schema::{community_actions, person}; diesel::alias!( + community_actions as creator_community_actions: CreatorCommunityActions, person as person1: Person1, person as person2: Person2, - community_moderator as community_moderator1: CommunityModerator1 ); } pub mod source; @@ -215,6 +215,8 @@ pub enum ModlogActionType { AdminPurgeCommunity, AdminPurgePost, AdminPurgeComment, + AdminBlockInstance, + AdminAllowInstance, } #[derive( diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 7c91cc905..ed9297ec3 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -42,6 +42,29 @@ pub mod sql_types { pub struct RegistrationModeEnum; } +diesel::table! { + admin_allow_instance (id) { + id -> Int4, + instance_id -> Int4, + admin_person_id -> Int4, + allowed -> Bool, + reason -> Nullable, + when_ -> Timestamptz, + } +} + +diesel::table! { + admin_block_instance (id) { + id -> Int4, + instance_id -> Int4, + admin_person_id -> Int4, + blocked -> Bool, + reason -> Nullable, + expires -> Nullable, + when_ -> Timestamptz, + } +} + diesel::table! { admin_purge_comment (id) { id -> Int4, @@ -110,6 +133,16 @@ diesel::table! { } } +diesel::table! { + comment_actions (person_id, comment_id) { + person_id -> Int4, + comment_id -> Int4, + like_score -> Nullable, + liked -> Nullable, + saved -> Nullable, + } +} + diesel::table! { comment_aggregates (comment_id) { comment_id -> Int4, @@ -120,15 +153,8 @@ diesel::table! { child_count -> Int4, hot_rank -> Float8, controversy_rank -> Float8, - } -} - -diesel::table! { - comment_like (person_id, comment_id) { - person_id -> Int4, - comment_id -> Int4, - score -> Int2, - published -> Timestamptz, + report_count -> Int2, + unresolved_report_count -> Int2, } } @@ -156,14 +182,6 @@ diesel::table! { } } -diesel::table! { - comment_saved (person_id, comment_id) { - comment_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { use diesel::sql_types::*; use super::sql_types::CommunityVisibility; @@ -205,6 +223,23 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::CommunityFollowerState; + + community_actions (person_id, community_id) { + community_id -> Int4, + person_id -> Int4, + followed -> Nullable, + follow_state -> Nullable, + follow_approver_id -> Nullable, + blocked -> Nullable, + became_moderator -> Nullable, + received_ban -> Nullable, + ban_expires -> Nullable, + } +} + diesel::table! { community_aggregates (community_id) { community_id -> Int4, @@ -221,27 +256,6 @@ diesel::table! { } } -diesel::table! { - community_block (person_id, community_id) { - person_id -> Int4, - community_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::CommunityFollowerState; - - community_follower (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - state -> CommunityFollowerState, - approver_id -> Nullable, - } -} - diesel::table! { community_language (community_id, language_id) { community_id -> Int4, @@ -249,23 +263,6 @@ diesel::table! { } } -diesel::table! { - community_moderator (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - community_person_ban (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - expires -> Nullable, - } -} - diesel::table! { community_post_tag (id) { id -> Int4, @@ -322,6 +319,7 @@ diesel::table! { instance_id -> Int4, published -> Timestamptz, updated -> Nullable, + expires -> Nullable, } } @@ -359,10 +357,10 @@ diesel::table! { } diesel::table! { - instance_block (person_id, instance_id) { + instance_actions (person_id, instance_id) { person_id -> Int4, instance_id -> Int4, - published -> Timestamptz, + blocked -> Nullable, } } @@ -495,6 +493,7 @@ diesel::table! { enable_private_messages -> Bool, collapse_bot_comments -> Bool, default_comment_sort_type -> CommentSortTypeEnum, + auto_mark_fetched_posts_as_read -> Bool, } } @@ -714,6 +713,16 @@ diesel::table! { } } +diesel::table! { + person_actions (person_id, target_id) { + target_id -> Int4, + person_id -> Int4, + followed -> Nullable, + follow_pending -> Nullable, + blocked -> Nullable, + } +} + diesel::table! { person_aggregates (person_id) { person_id -> Int4, @@ -731,23 +740,6 @@ diesel::table! { } } -diesel::table! { - person_block (person_id, target_id) { - person_id -> Int4, - target_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - person_follower (follower_id, person_id) { - person_id -> Int4, - follower_id -> Int4, - published -> Timestamptz, - pending -> Bool, - } -} - diesel::table! { person_mention (id) { id -> Int4, @@ -758,15 +750,6 @@ diesel::table! { } } -diesel::table! { - person_post_aggregates (person_id, post_id) { - person_id -> Int4, - post_id -> Int4, - read_comments -> Int8, - published -> Timestamptz, - } -} - diesel::table! { post (id) { id -> Int4, @@ -799,6 +782,20 @@ diesel::table! { } } +diesel::table! { + post_actions (person_id, post_id) { + post_id -> Int4, + person_id -> Int4, + read -> Nullable, + read_comments -> Nullable, + read_comments_amount -> Nullable, + saved -> Nullable, + liked -> Nullable, + like_score -> Nullable, + hidden -> Nullable, + } +} + diesel::table! { post_aggregates (post_id) { post_id -> Int4, @@ -818,6 +815,8 @@ diesel::table! { controversy_rank -> Float8, instance_id -> Int4, scaled_rank -> Float8, + report_count -> Int2, + unresolved_report_count -> Int2, } } @@ -828,31 +827,6 @@ diesel::table! { } } -diesel::table! { - post_hide (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - post_like (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - score -> Int2, - published -> Timestamptz, - } -} - -diesel::table! { - post_read (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { post_report (id) { id -> Int4, @@ -870,14 +844,6 @@ diesel::table! { } } -diesel::table! { - post_saved (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { private_message (id) { id -> Int4, @@ -1012,6 +978,10 @@ diesel::table! { } } +diesel::joinable!(admin_allow_instance -> instance (instance_id)); +diesel::joinable!(admin_allow_instance -> person (admin_person_id)); +diesel::joinable!(admin_block_instance -> instance (instance_id)); +diesel::joinable!(admin_block_instance -> person (admin_person_id)); diesel::joinable!(admin_purge_comment -> person (admin_person_id)); diesel::joinable!(admin_purge_comment -> post (post_id)); diesel::joinable!(admin_purge_community -> person (admin_person_id)); @@ -1021,33 +991,25 @@ diesel::joinable!(admin_purge_post -> person (admin_person_id)); diesel::joinable!(comment -> language (language_id)); diesel::joinable!(comment -> person (creator_id)); diesel::joinable!(comment -> post (post_id)); +diesel::joinable!(comment_actions -> comment (comment_id)); +diesel::joinable!(comment_actions -> person (person_id)); diesel::joinable!(comment_aggregates -> comment (comment_id)); -diesel::joinable!(comment_like -> comment (comment_id)); -diesel::joinable!(comment_like -> person (person_id)); diesel::joinable!(comment_reply -> comment (comment_id)); diesel::joinable!(comment_reply -> person (recipient_id)); diesel::joinable!(comment_report -> comment (comment_id)); -diesel::joinable!(comment_saved -> comment (comment_id)); -diesel::joinable!(comment_saved -> person (person_id)); diesel::joinable!(community -> instance (instance_id)); +diesel::joinable!(community_actions -> community (community_id)); diesel::joinable!(community_aggregates -> community (community_id)); -diesel::joinable!(community_block -> community (community_id)); -diesel::joinable!(community_block -> person (person_id)); -diesel::joinable!(community_follower -> community (community_id)); diesel::joinable!(community_language -> community (community_id)); diesel::joinable!(community_language -> language (language_id)); -diesel::joinable!(community_moderator -> community (community_id)); -diesel::joinable!(community_moderator -> person (person_id)); -diesel::joinable!(community_person_ban -> community (community_id)); -diesel::joinable!(community_person_ban -> person (person_id)); diesel::joinable!(community_post_tag -> community (community_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id)); diesel::joinable!(federation_queue_state -> instance (instance_id)); -diesel::joinable!(instance_block -> instance (instance_id)); -diesel::joinable!(instance_block -> person (person_id)); +diesel::joinable!(instance_actions -> instance (instance_id)); +diesel::joinable!(instance_actions -> person (person_id)); diesel::joinable!(local_image -> local_user (local_user_id)); diesel::joinable!(local_site -> site (site_id)); diesel::joinable!(local_site_rate_limit -> local_site (local_site_id)); @@ -1079,26 +1041,18 @@ diesel::joinable!(person_aggregates -> person (person_id)); diesel::joinable!(person_ban -> person (person_id)); diesel::joinable!(person_mention -> comment (comment_id)); diesel::joinable!(person_mention -> person (recipient_id)); -diesel::joinable!(person_post_aggregates -> person (person_id)); -diesel::joinable!(person_post_aggregates -> post (post_id)); diesel::joinable!(post -> community (community_id)); diesel::joinable!(post -> language (language_id)); diesel::joinable!(post -> person (creator_id)); +diesel::joinable!(post_actions -> person (person_id)); +diesel::joinable!(post_actions -> post (post_id)); diesel::joinable!(post_aggregates -> community (community_id)); diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); diesel::joinable!(post_community_post_tag -> community_post_tag (community_post_tag_id)); diesel::joinable!(post_community_post_tag -> post (post_id)); -diesel::joinable!(post_hide -> person (person_id)); -diesel::joinable!(post_hide -> post (post_id)); -diesel::joinable!(post_like -> person (person_id)); -diesel::joinable!(post_like -> post (post_id)); -diesel::joinable!(post_read -> person (person_id)); -diesel::joinable!(post_read -> post (post_id)); diesel::joinable!(post_report -> post (post_id)); -diesel::joinable!(post_saved -> person (person_id)); -diesel::joinable!(post_saved -> post (post_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); @@ -1108,24 +1062,22 @@ diesel::joinable!(site_language -> language (language_id)); diesel::joinable!(site_language -> site (site_id)); diesel::allow_tables_to_appear_in_same_query!( + admin_allow_instance, + admin_block_instance, admin_purge_comment, admin_purge_community, admin_purge_person, admin_purge_post, captcha_answer, comment, + comment_actions, comment_aggregates, - comment_like, comment_reply, comment_report, - comment_saved, community, + community_actions, community_aggregates, - community_block, - community_follower, community_language, - community_moderator, - community_person_ban, community_post_tag, custom_emoji, custom_emoji_keyword, @@ -1135,7 +1087,7 @@ diesel::allow_tables_to_appear_in_same_query!( federation_queue_state, image_details, instance, - instance_block, + instance_actions, language, local_image, local_site, @@ -1160,20 +1112,15 @@ diesel::allow_tables_to_appear_in_same_query!( oauth_provider, password_reset_request, person, + person_actions, person_aggregates, person_ban, - person_block, - person_follower, person_mention, - person_post_aggregates, post, + post_actions, post_aggregates, post_community_post_tag, - post_hide, - post_like, - post_read, post_report, - post_saved, private_message, private_message_report, received_activity, diff --git a/crates/db_schema/src/source/comment.rs b/crates/db_schema/src/source/comment.rs index 7e65638ed..d4001807f 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -2,9 +2,11 @@ use crate::newtypes::LtreeDef; use crate::newtypes::{CommentId, DbUrl, LanguageId, PersonId, PostId}; #[cfg(feature = "full")] -use crate::schema::{comment, comment_like, comment_saved}; +use crate::schema::{comment, comment_actions}; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; +#[cfg(feature = "full")] use diesel_ltree::Ltree; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -82,7 +84,7 @@ pub struct CommentInsertForm { pub struct CommentUpdateForm { pub content: Option, pub removed: Option, - // Don't use a default naive_now here, because the create function does a lot of comment updates + // Don't use a default Utc::now here, because the create function does a lot of comment updates pub updated: Option>>, pub deleted: Option, pub ap_id: Option, @@ -97,22 +99,27 @@ pub struct CommentUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(table_name = comment_like))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, comment_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommentLike { pub person_id: PersonId, pub comment_id: CommentId, + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::like_score.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub score: i16, + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::liked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = comment_like))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] pub struct CommentLikeForm { pub person_id: PersonId, pub comment_id: CommentId, + #[cfg_attr(feature = "full", diesel(column_name = like_score))] pub score: i16, } @@ -122,17 +129,19 @@ pub struct CommentLikeForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(table_name = comment_saved))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, comment_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommentSaved { pub comment_id: CommentId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::saved.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = comment_saved))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] pub struct CommentSavedForm { pub comment_id: CommentId, pub person_id: PersonId, diff --git a/crates/db_schema/src/source/community.rs b/crates/db_schema/src/source/community.rs index 95d2a67c3..f65ef06f9 100644 --- a/crates/db_schema/src/source/community.rs +++ b/crates/db_schema/src/source/community.rs @@ -1,5 +1,5 @@ #[cfg(feature = "full")] -use crate::schema::{community, community_follower, community_moderator, community_person_ban}; +use crate::schema::{community, community_actions}; use crate::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, sensitive::SensitiveString, @@ -7,6 +7,8 @@ use crate::{ CommunityVisibility, }; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use strum::{Display, EnumString}; @@ -163,18 +165,20 @@ pub struct CommunityUpdateForm { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_moderator))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityModerator { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::became_moderator.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_moderator))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityModeratorForm { pub community_id: CommunityId, pub person_id: PersonId, @@ -189,22 +193,26 @@ pub struct CommunityModeratorForm { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_person_ban))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityPersonBan { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::received_ban.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, + #[cfg_attr(feature = "full", diesel(column_name = ban_expires))] pub expires: Option>, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_person_ban))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityPersonBanForm { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = ban_expires))] pub expires: Option>>, } @@ -231,25 +239,32 @@ pub enum CommunityFollowerState { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_follower))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityFollower { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::followed.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::follow_state.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub state: CommunityFollowerState, + #[cfg_attr(feature = "full", diesel(column_name = follow_approver_id))] pub approver_id: Option, } #[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_follower))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityFollowerForm { pub community_id: CommunityId, pub person_id: PersonId, #[new(default)] + #[cfg_attr(feature = "full", diesel(column_name = follow_state))] pub state: Option, #[new(default)] + #[cfg_attr(feature = "full", diesel(column_name = follow_approver_id))] pub approver_id: Option, } diff --git a/crates/db_schema/src/source/community_block.rs b/crates/db_schema/src/source/community_block.rs index 7d43af173..a7c23419c 100644 --- a/crates/db_schema/src/source/community_block.rs +++ b/crates/db_schema/src/source/community_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::{CommunityId, PersonId}; #[cfg(feature = "full")] -use crate::schema::community_block; +use crate::schema::community_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -13,17 +15,19 @@ use serde::{Deserialize, Serialize}; feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_block))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityBlock { pub person_id: PersonId, pub community_id: CommunityId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_block))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityBlockForm { pub person_id: PersonId, pub community_id: CommunityId, diff --git a/crates/db_schema/src/source/federation_blocklist.rs b/crates/db_schema/src/source/federation_blocklist.rs index 2176ce42d..df877facf 100644 --- a/crates/db_schema/src/source/federation_blocklist.rs +++ b/crates/db_schema/src/source/federation_blocklist.rs @@ -1,14 +1,14 @@ use crate::newtypes::InstanceId; -#[cfg(feature = "full")] -use crate::schema::federation_blocklist; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; +#[cfg(feature = "full")] +use {crate::schema::federation_blocklist, ts_rs::TS}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[cfg_attr( feature = "full", - derive(Queryable, Selectable, Associations, Identifiable) + derive(TS, Queryable, Selectable, Associations, Identifiable) )] #[cfg_attr( feature = "full", @@ -17,10 +17,14 @@ use std::fmt::Debug; #[cfg_attr(feature = "full", diesel(table_name = federation_blocklist))] #[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] pub struct FederationBlockList { pub instance_id: InstanceId, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, } #[derive(Clone, Default)] @@ -29,4 +33,5 @@ pub struct FederationBlockList { pub struct FederationBlockListForm { pub instance_id: InstanceId, pub updated: Option>, + pub expires: Option>, } diff --git a/crates/db_schema/src/source/instance_block.rs b/crates/db_schema/src/source/instance_block.rs index 4eebbf1a8..e1963c894 100644 --- a/crates/db_schema/src/source/instance_block.rs +++ b/crates/db_schema/src/source/instance_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::{InstanceId, PersonId}; #[cfg(feature = "full")] -use crate::schema::instance_block; +use crate::schema::instance_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -13,17 +15,19 @@ use serde::{Deserialize, Serialize}; feature = "full", diesel(belongs_to(crate::source::instance::Instance)) )] -#[cfg_attr(feature = "full", diesel(table_name = instance_block))] +#[cfg_attr(feature = "full", diesel(table_name = instance_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, instance_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct InstanceBlock { pub person_id: PersonId, pub instance_id: InstanceId, + #[cfg_attr(feature = "full", diesel(select_expression = instance_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = instance_block))] +#[cfg_attr(feature = "full", diesel(table_name = instance_actions))] pub struct InstanceBlockForm { pub person_id: PersonId, pub instance_id: InstanceId, diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index fd15253cc..82d16f7d4 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -68,6 +68,8 @@ pub struct LocalUser { /// Whether to auto-collapse bot comments. pub collapse_bot_comments: bool, pub default_comment_sort_type: CommentSortType, + /// Whether to automatically mark fetched posts as read. + pub auto_mark_fetched_posts_as_read: bool, } #[derive(Clone, derive_new::new)] @@ -124,6 +126,8 @@ pub struct LocalUserInsertForm { pub collapse_bot_comments: Option, #[new(default)] pub default_comment_sort_type: Option, + #[new(default)] + pub auto_mark_fetched_posts_as_read: Option, } #[derive(Clone, Default)] @@ -155,4 +159,5 @@ pub struct LocalUserUpdateForm { pub enable_private_messages: Option, pub collapse_bot_comments: Option, pub default_comment_sort_type: Option, + pub auto_mark_fetched_posts_as_read: Option, } diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 3b11237ae..c976c1527 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -27,7 +27,7 @@ pub mod local_site_url_blocklist; pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; -pub mod moderator; +pub mod mod_log; pub mod oauth_account; pub mod oauth_provider; pub mod password_reset_request; @@ -48,6 +48,7 @@ pub mod tagline; /// 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. +#[allow(clippy::expect_used)] 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/mod_log/admin.rs b/crates/db_schema/src/source/mod_log/admin.rs new file mode 100644 index 000000000..d6e48b8ee --- /dev/null +++ b/crates/db_schema/src/source/mod_log/admin.rs @@ -0,0 +1,176 @@ +use crate::newtypes::{CommunityId, InstanceId, PersonId, PostId}; +#[cfg(feature = "full")] +use crate::schema::{ + admin_allow_instance, + admin_block_instance, + admin_purge_comment, + admin_purge_community, + admin_purge_person, + admin_purge_post, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a person. +pub struct AdminPurgePerson { + pub id: i32, + pub admin_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] +pub struct AdminPurgePersonForm { + pub admin_person_id: PersonId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a community. +pub struct AdminPurgeCommunity { + pub id: i32, + pub admin_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] +pub struct AdminPurgeCommunityForm { + pub admin_person_id: PersonId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a post. +pub struct AdminPurgePost { + pub id: i32, + pub admin_person_id: PersonId, + pub community_id: CommunityId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] +pub struct AdminPurgePostForm { + pub admin_person_id: PersonId, + pub community_id: CommunityId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a comment. +pub struct AdminPurgeComment { + pub id: i32, + pub admin_person_id: PersonId, + pub post_id: PostId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] +pub struct AdminPurgeCommentForm { + pub admin_person_id: PersonId, + pub post_id: PostId, + pub reason: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr( + feature = "full", + derive(TS, Queryable, Selectable, Associations, Identifiable) +)] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::instance::Instance)) +)] +#[cfg_attr(feature = "full", diesel(table_name = admin_allow_instance))] +#[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminAllowInstance { + pub id: i32, + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub allowed: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_allow_instance))] +pub struct AdminAllowInstanceForm { + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub allowed: bool, + pub reason: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr( + feature = "full", + derive(TS, Queryable, Selectable, Associations, Identifiable) +)] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::instance::Instance)) +)] +#[cfg_attr(feature = "full", diesel(table_name = admin_block_instance))] +#[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminBlockInstance { + pub id: i32, + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, + pub when_: DateTime, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_block_instance))] +pub struct AdminBlockInstanceForm { + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub blocked: bool, + pub reason: Option, + pub when_: Option>, +} diff --git a/crates/db_schema/src/source/mod_log/mod.rs b/crates/db_schema/src/source/mod_log/mod.rs new file mode 100644 index 000000000..54341c69a --- /dev/null +++ b/crates/db_schema/src/source/mod_log/mod.rs @@ -0,0 +1,2 @@ +pub mod admin; +pub mod moderator; diff --git a/crates/db_schema/src/source/moderator.rs b/crates/db_schema/src/source/mod_log/moderator.rs similarity index 75% rename from crates/db_schema/src/source/moderator.rs rename to crates/db_schema/src/source/mod_log/moderator.rs index b4fdcc676..470b643a5 100644 --- a/crates/db_schema/src/source/moderator.rs +++ b/crates/db_schema/src/source/mod_log/moderator.rs @@ -1,10 +1,6 @@ use crate::newtypes::{CommentId, CommunityId, PersonId, PostId}; #[cfg(feature = "full")] use crate::schema::{ - admin_purge_comment, - admin_purge_community, - admin_purge_person, - admin_purge_post, mod_add, mod_add_community, mod_ban, @@ -300,95 +296,3 @@ pub struct ModAddForm { pub other_person_id: PersonId, pub removed: Option, } - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a person. -pub struct AdminPurgePerson { - pub id: i32, - pub admin_person_id: PersonId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] -pub struct AdminPurgePersonForm { - pub admin_person_id: PersonId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a community. -pub struct AdminPurgeCommunity { - pub id: i32, - pub admin_person_id: PersonId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] -pub struct AdminPurgeCommunityForm { - pub admin_person_id: PersonId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a post. -pub struct AdminPurgePost { - pub id: i32, - pub admin_person_id: PersonId, - pub community_id: CommunityId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] -pub struct AdminPurgePostForm { - pub admin_person_id: PersonId, - pub community_id: CommunityId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a comment. -pub struct AdminPurgeComment { - pub id: i32, - pub admin_person_id: PersonId, - pub post_id: PostId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] -pub struct AdminPurgeCommentForm { - pub admin_person_id: PersonId, - pub post_id: PostId, - pub reason: Option, -} diff --git a/crates/db_schema/src/source/person.rs b/crates/db_schema/src/source/person.rs index d8b0a5b1a..9c2a2d426 100644 --- a/crates/db_schema/src/source/person.rs +++ b/crates/db_schema/src/source/person.rs @@ -1,11 +1,13 @@ #[cfg(feature = "full")] -use crate::schema::{person, person_follower}; +use crate::schema::{person, person_actions}; use crate::{ newtypes::{DbUrl, InstanceId, PersonId}, sensitive::SensitiveString, source::placeholder_apub_url, }; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -133,21 +135,30 @@ pub struct PersonUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] -#[cfg_attr(feature = "full", diesel(table_name = person_follower))] -#[cfg_attr(feature = "full", diesel(primary_key(follower_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, target_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PersonFollower { + #[cfg_attr(feature = "full", diesel(column_name = target_id))] pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = person_id))] pub follower_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::followed.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::follow_pending.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub pending: bool, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_follower))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] pub struct PersonFollowerForm { + #[cfg_attr(feature = "full", diesel(column_name = target_id))] pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = person_id))] pub follower_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = follow_pending))] pub pending: bool, } diff --git a/crates/db_schema/src/source/person_block.rs b/crates/db_schema/src/source/person_block.rs index 43048fb39..ec988a60f 100644 --- a/crates/db_schema/src/source/person_block.rs +++ b/crates/db_schema/src/source/person_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::PersonId; #[cfg(feature = "full")] -use crate::schema::person_block; +use crate::schema::person_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -10,17 +12,19 @@ use serde::{Deserialize, Serialize}; derive(Queryable, Selectable, Associations, Identifiable) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] -#[cfg_attr(feature = "full", diesel(table_name = person_block))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, target_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PersonBlock { pub person_id: PersonId, pub target_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_block))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] pub struct PersonBlockForm { pub person_id: PersonId, pub target_id: PersonId, diff --git a/crates/db_schema/src/source/post.rs b/crates/db_schema/src/source/post.rs index 3417f87b5..306d79e70 100644 --- a/crates/db_schema/src/source/post.rs +++ b/crates/db_schema/src/source/post.rs @@ -1,7 +1,9 @@ use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId}; #[cfg(feature = "full")] -use crate::schema::{post, post_hide, post_like, post_read, post_saved}; +use crate::schema::{post, post_actions}; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -149,23 +151,30 @@ pub struct PostUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_like))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostLike { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::like_score.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub score: i16, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::liked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } -#[derive(Clone)] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_like))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PostLikeForm { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = like_score))] pub score: i16, + #[new(value = "Utc::now()")] + pub liked: DateTime, } #[derive(PartialEq, Eq, Debug)] @@ -174,20 +183,25 @@ pub struct PostLikeForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_saved))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostSaved { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::saved.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } +#[derive(derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_saved))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PostSavedForm { pub post_id: PostId, pub person_id: PersonId, + #[new(value = "Utc::now()")] + pub saved: DateTime, } #[derive(PartialEq, Eq, Debug)] @@ -196,20 +210,25 @@ pub struct PostSavedForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_read))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostRead { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } +#[derive(derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_read))] -pub(crate) struct PostReadForm { +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +pub struct PostReadForm { pub post_id: PostId, pub person_id: PersonId, + #[new(value = "Utc::now()")] + pub read: DateTime, } #[derive(PartialEq, Eq, Debug)] @@ -218,18 +237,23 @@ pub(crate) struct PostReadForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_hide))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostHide { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::hidden.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } +#[derive(derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_hide))] -pub(crate) struct PostHideForm { +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +pub struct PostHideForm { pub post_id: PostId, pub person_id: PersonId, + #[new(value = "Utc::now()")] + pub hidden: DateTime, } diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index 74f5ea009..bc30c6fb9 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{CommunityId, DbUrl, PersonId}, - utils::{get_conn, DbPool}, + utils::{get_conn, uplete, DbPool}, }; use diesel::{ associations::HasTable, @@ -76,7 +76,7 @@ pub trait Followable { ) -> Result where Self: Sized; - async fn unfollow(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unfollow(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -87,7 +87,7 @@ pub trait Joinable { async fn join(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn leave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn leave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -103,7 +103,7 @@ pub trait Likeable { pool: &mut DbPool<'_>, person_id: PersonId, item_id: Self::IdType, - ) -> Result + ) -> Result where Self: Sized; } @@ -114,7 +114,7 @@ pub trait Bannable { async fn ban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -125,7 +125,7 @@ pub trait Saveable { async fn save(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unsave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unsave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -136,7 +136,7 @@ pub trait Blockable { async fn block(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unblock(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unblock(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index c6acb59eb..5bbf007ae 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,18 +1,30 @@ +pub mod uplete; + use crate::{newtypes::DbUrl, CommentSortType, PostSortType}; -use chrono::{DateTime, TimeDelta, Utc}; +use chrono::TimeDelta; use deadpool::Runtime; use diesel::{ + dsl, + expression::AsExpression, helper_types::AsExprOf, pg::Pg, query_builder::{Query, QueryFragment}, - query_dsl::methods::LimitDsl, + query_dsl::methods::{FilterDsl, FindDsl, LimitDsl}, + query_source::{Alias, AliasSource, AliasedField}, result::{ ConnectionError, ConnectionResult, Error::{self as DieselError, QueryBuilderError}, }, - sql_types::{self, Timestamptz}, + sql_types::{self, SingleValue, Timestamptz}, + Column, + Expression, + ExpressionMethods, IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QuerySource, + Table, }; use diesel_async::{ pg::AsyncPgConnection, @@ -23,6 +35,7 @@ use diesel_async::{ }, AsyncConnection, }; +use diesel_bind_if_some::BindIfSome; use futures_util::{future::BoxFuture, Future, FutureExt}; use i_love_jesus::CursorKey; use lemmy_utils::{ @@ -55,7 +68,7 @@ use url::Url; const FETCH_LIMIT_DEFAULT: i64 = 10; pub const FETCH_LIMIT_MAX: i64 = 50; pub const SITEMAP_LIMIT: i64 = 50000; -pub const SITEMAP_DAYS: Option = TimeDelta::try_days(31); +pub const SITEMAP_DAYS: TimeDelta = TimeDelta::days(31); pub const RANK_DEFAULT: f64 = 0.0001; /// Some connection options to speed up queries @@ -83,7 +96,7 @@ pub async fn get_conn<'a, 'b: 'a>(pool: &'a mut DbPool<'b>) -> Result }) } -impl<'a> Deref for DbConn<'a> { +impl Deref for DbConn<'_> { type Target = AsyncPgConnection; fn deref(&self) -> &Self::Target { @@ -94,7 +107,7 @@ impl<'a> Deref for DbConn<'a> { } } -impl<'a> DerefMut for DbConn<'a> { +impl DerefMut for DbConn<'_> { fn deref_mut(&mut self) -> &mut Self::Target { match self { DbConn::Pool(conn) => conn.deref_mut(), @@ -168,8 +181,8 @@ where K: CursorKey, { type SqlType = sql_types::BigInt; - type CursorValue = functions::reverse_timestamp_sort::HelperType; - type SqlValue = functions::reverse_timestamp_sort::HelperType; + type CursorValue = functions::reverse_timestamp_sort; + type SqlValue = functions::reverse_timestamp_sort; fn get_cursor_value(cursor: &C) -> Self::CursorValue { functions::reverse_timestamp_sort(K::get_cursor_value(cursor)) @@ -347,8 +360,8 @@ pub fn diesel_url_create(opt: Option<&str>) -> LemmyResult> { } /// Sets a few additional config options necessary for starting lemmy -fn build_config_options_uri_segment(config: &str) -> String { - let mut url = Url::parse(config).expect("Couldn't parse postgres connection URI"); +fn build_config_options_uri_segment(config: &str) -> LemmyResult { + let mut url = Url::parse(config)?; // Set `lemmy.protocol_and_hostname` so triggers can use it let lemmy_protocol_and_hostname_option = @@ -364,7 +377,7 @@ fn build_config_options_uri_segment(config: &str) -> String { .join(" "); url.set_query(Some(&format!("options={options_segments}"))); - url.into() + Ok(url.into()) } fn establish_connection(config: &str) -> BoxFuture> { @@ -372,8 +385,11 @@ fn establish_connection(config: &str) -> BoxFuture = OnceLock::new(); - let config = - POSTGRES_CONFIG_WITH_OPTIONS.get_or_init(|| build_config_options_uri_segment(config)); + let config = POSTGRES_CONFIG_WITH_OPTIONS.get_or_init(|| { + build_config_options_uri_segment(config) + .inspect_err(|e| error!("Couldn't parse postgres connection URI: {e}")) + .unwrap_or_default() + }); // We only support TLS with sslmode=require currently let conn = if config.contains("sslmode=require") { @@ -470,7 +486,7 @@ pub fn build_db_pool() -> LemmyResult { // 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)) + Err(HookError::Message("Connection is too old".into())) } else { Ok(()) } @@ -482,14 +498,11 @@ pub fn build_db_pool() -> LemmyResult { Ok(pool) } +#[allow(clippy::expect_used)] pub fn build_db_pool_for_tests() -> ActualDbPool { build_db_pool().expect("db pool missing") } -pub fn naive_now() -> DateTime { - Utc::now() -} - pub fn post_to_comment_sort_type(sort: PostSortType) -> CommentSortType { use PostSortType::*; match sort { @@ -502,6 +515,7 @@ pub fn post_to_comment_sort_type(sort: PostSortType) -> CommentSortType { } } +#[allow(clippy::expect_used)] static EMAIL_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$") .expect("compile email regex") @@ -510,29 +524,31 @@ static EMAIL_REGEX: LazyLock = LazyLock::new(|| { pub mod functions { use diesel::sql_types::{BigInt, Text, Timestamptz}; - sql_function! { + define_sql_function! { #[sql_name = "r.hot_rank"] fn hot_rank(score: BigInt, time: Timestamptz) -> Double; } - sql_function! { + define_sql_function! { #[sql_name = "r.scaled_rank"] fn scaled_rank(score: BigInt, time: Timestamptz, users_active_month: BigInt) -> Double; } - sql_function! { + define_sql_function! { #[sql_name = "r.controversy_rank"] fn controversy_rank(upvotes: BigInt, downvotes: BigInt, score: BigInt) -> Double; } - sql_function!(fn reverse_timestamp_sort(time: Timestamptz) -> BigInt); + define_sql_function!(fn reverse_timestamp_sort(time: Timestamptz) -> BigInt); - sql_function!(fn lower(x: Text) -> Text); + define_sql_function!(fn lower(x: Text) -> Text); + + define_sql_function!(fn random() -> Text); // really this function is variadic, this just adds the two-argument version - sql_function!(fn coalesce(x: diesel::sql_types::Nullable, y: T) -> T); + define_sql_function!(fn coalesce(x: diesel::sql_types::Nullable, y: T) -> T); - sql_function! { + define_sql_function! { #[aggregate] fn json_agg(obj: T) -> Json } @@ -545,6 +561,117 @@ pub fn now() -> AsExprOf { diesel::dsl::now.into_sql::() } +/// Trait alias for a type that can be converted to an SQL tuple using `IntoSql::into_sql` +pub trait AsRecord: Expression + AsExpression> +where + Self::SqlType: 'static, +{ +} + +impl>> AsRecord for T where + T::SqlType: 'static +{ +} + +/// Output of `IntoSql::into_sql` for a type that implements `AsRecord` +pub type AsRecordOutput = dsl::AsExprOf::SqlType>>; + +/// Output of `t.on((l0, l1).into_sql().eq((r0, r1)))` +type OnTupleEq = dsl::On, (R0, R1)>>; + +/// Creates an `ON` clause for a table where a person ID and another column are used as the +/// primary key. Use with the `QueryDsl::left_join` method. +/// +/// This example modifies a query to make columns in `community_actions` available: +/// +/// ``` +/// community::table +/// .left_join(actions( +/// community_actions::table, +/// my_person_id, +/// community::id, +/// )) +/// ``` +pub fn actions( + actions_table: T, + person_id: Option

, + target_id: C, +) -> OnTupleEq, K1, BindIfSome>, C> +where + T: Table + Copy, + K0: Expression, + P: AsExpression, + (dsl::Nullable, K1): AsRecord, + (BindIfSome>, C): + AsExpression<, K1)> as Expression>::SqlType>, +{ + let (k0, k1) = actions_table.primary_key(); + actions_table.on((k0.nullable(), k1).into_sql().eq(( + BindIfSome(person_id.map(diesel::IntoSql::into_sql)), + target_id, + ))) +} + +/// Like `actions` but `actions_table` is an alias and person id is not nullable +#[allow(clippy::type_complexity)] +pub fn actions_alias( + actions_table: Alias, + person_id: P, + target_id: C, +) -> OnTupleEq, AliasedField, AliasedField, P, C> +where + Alias: QuerySource + Copy, + T: AliasSource> + Default, + K0: Column, + K1: Column
, + (AliasedField, AliasedField): AsRecord, + (P, C): AsExpression< + , AliasedField)> as Expression>::SqlType, + >, +{ + let (k0, k1) = T::default().target().primary_key(); + actions_table.on( + (actions_table.field(k0), actions_table.field(k1)) + .into_sql() + .eq((person_id, target_id)), + ) +} + +/// `action_query(table_name::action_name)` is the same as +/// `table_name::table.filter(table_name::action_name.is_not_null())`. +pub fn action_query(column: C) -> dsl::Filter> +where + C: Column>, SqlType: SingleValue>, +{ + action_query_with_fn(column, |t| t) +} + +/// `find_action(table_name::action_name, key)` is the same as +/// `table_name::table.find(key).filter(table_name::action_name.is_not_null())`. +pub fn find_action( + column: C, + key: K, +) -> dsl::Filter, dsl::IsNotNull> +where + C: + Column>>, SqlType: SingleValue>, +{ + action_query_with_fn(column, |t| t.find(key)) +} + +/// `action_query_with_fn(table_name::action_name, f)` is the same as +/// `f(table_name::table).filter(table_name::action_name.is_not_null())`. +fn action_query_with_fn( + column: C, + f: impl FnOnce(C::Table) -> Q, +) -> dsl::Filter> +where + C: Column, + Q: FilterDsl>, +{ + f(C::Table::default()).filter(column.is_not_null()) +} + pub type ResultFuture<'a, T> = BoxFuture<'a, Result>; pub trait ReadFn<'a, T, Args>: Fn(DbConn<'a>, Args) -> ResultFuture<'a, T> {} diff --git a/crates/db_schema/src/utils/uplete.rs b/crates/db_schema/src/utils/uplete.rs new file mode 100644 index 000000000..8c5262b90 --- /dev/null +++ b/crates/db_schema/src/utils/uplete.rs @@ -0,0 +1,423 @@ +use diesel::{ + associations::HasTable, + dsl, + expression::{is_aggregate, ValidGrouping}, + pg::Pg, + query_builder::{AsQuery, AstPass, Query, QueryFragment, QueryId}, + query_dsl::methods::{FilterDsl, SelectDsl}, + result::Error, + sql_types, + Column, + Expression, + Table, +}; +use std::any::TypeId; +use tuplex::IntoArray; + +/// Set columns (each specified with `UpleteBuilder::set_null`) to null in the rows found by +/// `query`, and delete rows that have no remaining non-null values outside of the primary key +pub fn new(query: Q) -> UpleteBuilder::PrimaryKey>> +where + Q: AsQuery + HasTable, + Q::Table: Default, + Q::Query: SelectDsl<::PrimaryKey>, + + // For better error messages + UpleteBuilder: AsQuery, +{ + UpleteBuilder { + query: query.as_query().select(Q::Table::default().primary_key()), + set_null_columns: Vec::new(), + } +} + +pub struct UpleteBuilder { + query: Q, + set_null_columns: Vec, +} + +impl UpleteBuilder { + pub fn set_null + Into>(mut self, column: C) -> Self { + self.set_null_columns.push(column.into()); + self + } +} + +impl AsQuery for UpleteBuilder +where + Q: HasTable, + Q::Table: Default + QueryFragment + Send + 'static, + ::PrimaryKey: IntoArray + QueryFragment + Send + 'static, + ::AllColumns: IntoArray, + <::PrimaryKey as IntoArray>::Output: IntoIterator, + <::AllColumns as IntoArray>::Output: IntoIterator, + Q: Clone + FilterDsl + FilterDsl>, + dsl::Filter: QueryFragment + Send + 'static, + dsl::Filter>: QueryFragment + Send + 'static, +{ + type Query = UpleteQuery; + + type SqlType = (sql_types::BigInt, sql_types::BigInt); + + fn as_query(self) -> Self::Query { + let table = Q::Table::default; + let deletion_condition = AllNull( + Q::Table::all_columns() + .into_array() + .into_iter() + .filter(|c: &DynColumn| { + table() + .primary_key() + .into_array() + .into_iter() + .chain(self.set_null_columns.iter().cloned()) + .all(|excluded_column| excluded_column.type_id != c.type_id) + }) + .collect::>(), + ); + UpleteQuery { + // Updated rows and deleted rows must not overlap, so updating all rows and using the returned + // new rows to determine which ones to delete is not an option. + // + // https://www.postgresql.org/docs/16/queries-with.html#QUERIES-WITH-MODIFYING + // + // "Trying to update the same row twice in a single statement is not supported. Only one of + // the modifications takes place, but it is not easy (and sometimes not possible) to reliably + // predict which one. This also applies to deleting a row that was already updated in the same + // statement: only the update is performed." + update_subquery: Box::new( + self + .query + .clone() + .filter(dsl::not(deletion_condition.clone())), + ), + delete_subquery: Box::new(self.query.filter(deletion_condition)), + table: Box::new(table()), + primary_key: Box::new(table().primary_key()), + set_null_columns: self.set_null_columns, + } + } +} + +pub struct UpleteQuery { + update_subquery: Box + Send + 'static>, + delete_subquery: Box + Send + 'static>, + table: Box + Send + 'static>, + primary_key: Box + Send + 'static>, + set_null_columns: Vec, +} + +impl QueryId for UpleteQuery { + type QueryId = (); + + const HAS_STATIC_QUERY_ID: bool = false; +} + +impl Query for UpleteQuery { + type SqlType = (sql_types::BigInt, sql_types::BigInt); +} + +impl QueryFragment for UpleteQuery { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + assert_ne!(self.set_null_columns.len(), 0, "`set_null` was not called"); + + // Declare `update_keys` and `delete_keys` CTEs, which select primary keys + for (prefix, subquery) in [ + ("WITH update_keys", &self.update_subquery), + (", delete_keys", &self.delete_subquery), + ] { + out.push_sql(prefix); + out.push_sql(" AS ("); + subquery.walk_ast(out.reborrow())?; + out.push_sql(" FOR UPDATE)"); + } + + // Update rows that are referenced in `update_keys` + out.push_sql(", update_result AS (UPDATE "); + self.table.walk_ast(out.reborrow())?; + let mut item_prefix = " SET "; + for column in &self.set_null_columns { + out.push_sql(item_prefix); + out.push_identifier(column.name)?; + out.push_sql(" = NULL"); + item_prefix = ","; + } + out.push_sql(" WHERE ("); + self.primary_key.walk_ast(out.reborrow())?; + out.push_sql(") = ANY (SELECT * FROM update_keys) RETURNING 1)"); + + // Delete rows that are referenced in `delete_keys` + out.push_sql(", delete_result AS (DELETE FROM "); + self.table.walk_ast(out.reborrow())?; + out.push_sql(" WHERE ("); + self.primary_key.walk_ast(out.reborrow())?; + out.push_sql(") = ANY (SELECT * FROM delete_keys) RETURNING 1)"); + + // Count updated rows and deleted rows (`RETURNING 1` makes this possible) + out.push_sql(" SELECT (SELECT count(*) FROM update_result)"); + out.push_sql(", (SELECT count(*) FROM delete_result)"); + + Ok(()) + } +} + +// Types other than `DynColumn` are only used in tests +#[derive(Clone)] +pub struct AllNull(Vec); + +impl Expression for AllNull { + type SqlType = sql_types::Bool; +} + +impl ValidGrouping<()> for AllNull { + type IsAggregate = is_aggregate::No; +} + +impl> QueryFragment for AllNull { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + // Must produce a valid expression even if `self.0` is empty + out.push_sql("(TRUE"); + for item in &self.0 { + out.push_sql(" AND ("); + item.walk_ast(out.reborrow())?; + out.push_sql(" IS NULL)"); + } + out.push_sql(")"); + + Ok(()) + } +} + +#[derive(Clone)] +pub struct DynColumn { + type_id: TypeId, + name: &'static str, +} + +impl From for DynColumn { + fn from(_value: T) -> Self { + DynColumn { + type_id: TypeId::of::(), + name: T::NAME, + } + } +} + +impl QueryFragment for DynColumn { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + out.push_identifier(self.name) + } +} + +#[derive(Queryable, PartialEq, Eq, Debug)] +pub struct Count { + pub updated: i64, + pub deleted: i64, +} + +impl Count { + pub fn only_updated(n: i64) -> Self { + Count { + updated: n, + deleted: 0, + } + } + + pub fn only_deleted(n: i64) -> Self { + Count { + updated: 0, + deleted: n, + } + } +} + +#[cfg(test)] +mod tests { + use super::AllNull; + use crate::utils::{build_db_pool_for_tests, get_conn, DbConn}; + use diesel::{ + debug_query, + insert_into, + pg::Pg, + query_builder::{AsQuery, QueryId}, + select, + sql_types, + AppearsOnTable, + ExpressionMethods, + IntoSql, + QueryDsl, + SelectableExpression, + }; + use diesel_async::{RunQueryDsl, SimpleAsyncConnection}; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + impl AppearsOnTable for AllNull {} + + impl SelectableExpression for AllNull {} + + impl QueryId for AllNull { + type QueryId = (); + const HAS_STATIC_QUERY_ID: bool = false; + } + + diesel::table! { + t (id1, id2) { + // uplete doesn't work for non-tuple primary key + id1 -> Int4, + id2 -> Int4, + a -> Nullable, + b -> Nullable, + } + } + + async fn expect_rows( + conn: &mut DbConn<'_>, + expected: &[(Option, Option)], + ) -> LemmyResult<()> { + let rows: Vec<(Option, Option)> = t::table + .select((t::a, t::b)) + .order_by(t::id1) + .load(conn) + .await?; + assert_eq!(expected, &rows); + + Ok(()) + } + + // Main purpose of this test is to check accuracy of the returned `Count`, which other modules' + // tests rely on + #[tokio::test] + #[serial] + async fn test_count() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut conn = get_conn(pool).await?; + + conn + .batch_execute("CREATE TABLE t (id1 serial, id2 int NOT NULL DEFAULT 1, a int, b int, PRIMARY KEY (id1, id2));") + .await?; + expect_rows(&mut conn, &[]).await?; + + insert_into(t::table) + .values(&[ + (t::a.eq(Some(1)), t::b.eq(Some(2))), + (t::a.eq(Some(3)), t::b.eq(None)), + (t::a.eq(Some(4)), t::b.eq(Some(5))), + ]) + .execute(&mut conn) + .await?; + expect_rows( + &mut conn, + &[(Some(1), Some(2)), (Some(3), None), (Some(4), Some(5))], + ) + .await?; + + let count1 = super::new(t::table) + .set_null(t::a) + .get_result(&mut conn) + .await?; + assert_eq!( + super::Count { + updated: 2, + deleted: 1 + }, + count1 + ); + expect_rows(&mut conn, &[(None, Some(2)), (None, Some(5))]).await?; + + let count2 = super::new(t::table) + .set_null(t::b) + .get_result(&mut conn) + .await?; + assert_eq!(super::Count::only_deleted(2), count2); + expect_rows(&mut conn, &[]).await?; + + conn.batch_execute("DROP TABLE t;").await?; + + Ok(()) + } + + fn expected_sql(check_null: &str, set_null: &str) -> String { + let with_queries = { + let key = r#""t"."id1", "t"."id2""#; + let t = r#""t""#; + + let update_keys = format!("SELECT {key} FROM {t} WHERE NOT (({check_null})) FOR UPDATE"); + let delete_keys = format!("SELECT {key} FROM {t} WHERE ({check_null}) FOR UPDATE"); + let update_result = format!( + "UPDATE {t} SET {set_null} WHERE ({key}) = ANY (SELECT * FROM update_keys) RETURNING 1" + ); + let delete_result = + format!("DELETE FROM {t} WHERE ({key}) = ANY (SELECT * FROM delete_keys) RETURNING 1"); + + format!("update_keys AS ({update_keys}), delete_keys AS ({delete_keys}), update_result AS ({update_result}), delete_result AS ({delete_result})") + }; + let update_count = "SELECT count(*) FROM update_result"; + let delete_count = "SELECT count(*) FROM delete_result"; + + format!(r#"WITH {with_queries} SELECT ({update_count}), ({delete_count}) -- binds: []"#) + } + + #[test] + fn test_generated_sql() { + // Unlike the `get_result` method, `debug_query` does not automatically call `as_query` + assert_eq!( + debug_query::(&super::new(t::table).set_null(t::b).as_query()).to_string(), + expected_sql(r#"TRUE AND ("a" IS NULL)"#, r#""b" = NULL"#) + ); + assert_eq!( + debug_query::( + &super::new(t::table) + .set_null(t::a) + .set_null(t::b) + .as_query() + ) + .to_string(), + expected_sql(r#"TRUE"#, r#""a" = NULL,"b" = NULL"#) + ); + } + + #[test] + fn test_count_methods() { + assert_eq!( + super::Count::only_updated(1), + super::Count { + updated: 1, + deleted: 0 + } + ); + assert_eq!( + super::Count::only_deleted(1), + super::Count { + updated: 0, + deleted: 1 + } + ); + } + + #[tokio::test] + #[serial] + async fn test_all_null() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut conn = get_conn(pool).await?; + + let some = Some(1).into_sql::>(); + let none = None::.into_sql::>(); + + // Allows type inference for `vec![]` + let mut all_null = |items| select(AllNull(items)).get_result::(&mut conn); + + assert!(all_null(vec![]).await?); + assert!(all_null(vec![none]).await?); + assert!(all_null(vec![none, none]).await?); + assert!(all_null(vec![none, none, none]).await?); + assert!(!all_null(vec![some]).await?); + assert!(!all_null(vec![some, none]).await?); + assert!(!all_null(vec![none, some, none]).await?); + + Ok(()) + } +} diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 06c05639d..b4a23a0da 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -11,25 +11,33 @@ use diesel::{ }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommentId, CommentReportId, CommunityId, PersonId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, comment_report, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, post, }, source::community::CommunityFollower, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{ + actions, + actions_alias, + functions::coalesce, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, }; fn queries<'a>() -> Queries< @@ -46,40 +54,20 @@ fn queries<'a>() -> Queries< .inner_join( comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), ) - .left_join( - comment_like::table.on( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment_report::comment_id, + )) .left_join( aliases::person2 .on(comment_report::resolver_id.eq(aliases::person2.field(person::id).nullable())), ) - .left_join( - community_person_ban::table.on( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)) - .and( - community_person_ban::expires - .is_null() - .or(community_person_ban::expires.gt(now)), - ), - ), - ) - .left_join( - aliases::community_moderator1.on( - community::id - .eq(aliases::community_moderator1.field(community_moderator::community_id)) - .and( - aliases::community_moderator1 - .field(community_moderator::person_id) - .eq(comment::creator_id), - ), - ), - ) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .left_join( local_user::table.on( comment::creator_id @@ -87,27 +75,16 @@ fn queries<'a>() -> Queries< .and(local_user::admin.eq(true)), ), ) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - community_follower::table.on( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(my_person_id)), - ), - ) - .left_join( - comment_saved::table.on( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(my_person_id), + comment::creator_id, + )) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) .select(( comment_report::all_columns, comment::all_columns, @@ -116,16 +93,28 @@ fn queries<'a>() -> Queries< person::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - aliases::community_moderator1 - .field(community_moderator::community_id) + coalesce( + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null() + .or( + creator_community_actions + .field(community_actions::ban_expires) + .nullable() + .gt(now), + ), + false, + ), + creator_community_actions + .field(community_actions::became_moderator) .nullable() .is_not_null(), local_user::admin.nullable().is_not_null(), - person_block::target_id.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), CommunityFollower::select_subscribed_type(), - comment_saved::published.nullable().is_not_null(), - comment_like::score.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), aliases::person2.fields(person::all_columns).nullable(), )) }; @@ -167,19 +156,10 @@ fn queries<'a>() -> Queries< // If its not an admin, get only the ones you mod if !user.local_user.admin { - query - .inner_join( - community_moderator::table.on( - community_moderator::community_id - .eq(post::community_id) - .and(community_moderator::person_id.eq(user.person.id)), - ), - ) - .load::(&mut conn) - .await - } else { - query.load::(&mut conn).await + query = query.filter(community_actions::became_moderator.is_not_null()); } + + query.load::(&mut conn).await }; Queries::new(read, list) @@ -222,10 +202,11 @@ impl CommentReportView { if !admin { query .inner_join( - community_moderator::table.on( - community_moderator::community_id + community_actions::table.on( + community_actions::community_id .eq(post::community_id) - .and(community_moderator::person_id.eq(my_person_id)), + .and(community_actions::person_id.eq(my_person_id)) + .and(community_actions::became_moderator.is_not_null()), ), ) .select(count(comment_report::id)) @@ -463,6 +444,8 @@ mod tests { child_count: 0, hot_rank: RANK_DEFAULT, controversy_rank: 0.0, + report_count: 2, + unresolved_report_count: 2, }, my_vote: None, resolver: None, @@ -530,6 +513,10 @@ mod tests { .updated = read_jessica_report_view_after_resolve .comment_report .updated; + expected_jessica_report_view_after_resolve + .counts + .unresolved_report_count = 1; + expected_sara_report_view.counts.unresolved_report_count = 1; expected_jessica_report_view_after_resolve.resolver = Some(Person { id: inserted_timmy.id, name: inserted_timmy.name.clone(), diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 0521e401c..1037cf6ff 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -3,11 +3,8 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, PgTextExpressionMethods, @@ -16,23 +13,20 @@ use diesel::{ use diesel_async::RunQueryDsl; use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions}; use lemmy_db_schema::{ + aliases::creator_community_actions, impls::local_user::LocalUserOptionHelper, newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, - comment_saved, community, - community_block, - community_follower, - community_moderator, - community_person_ban, - instance_block, + community_actions, + instance_actions, local_user, local_user_language, person, - person_block, + person_actions, post, }, source::{ @@ -40,74 +34,29 @@ use lemmy_db_schema::{ local_user::LocalUser, site::Site, }, - utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{ + actions, + actions_alias, + fuzzy_search, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, CommentSortType, CommunityVisibility, ListingType, }; +type QueriesReadTypes<'a> = (CommentId, Option<&'a LocalUser>); +type QueriesListTypes<'a> = (CommentQuery<'a>, &'a Site); + fn queries<'a>() -> Queries< - impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>, - impl ListFn<'a, CommentView, (CommentQuery<'a>, &'a Site)>, + impl ReadFn<'a, CommentView, QueriesReadTypes<'a>>, + impl ListFn<'a, CommentView, QueriesListTypes<'a>>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(CommunityFollower::select_subscribed_type()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -117,67 +66,56 @@ fn queries<'a>() -> Queries< ); let all_joins = move |query: comment::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression< - _, - Pg, - SqlType = sql_types::Nullable, - >, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(person::table) .inner_join(post::table) .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(comment_aggregates::table) - .left_join( - comment_saved::table.on( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(my_person_id.unwrap_or(PersonId(-1)))), - ), - ) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions( + comment_actions::table, + my_person_id, + comment_aggregates::comment_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions( + instance_actions::table, + my_person_id, + community::instance_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( comment::all_columns, person::all_columns, post::all_columns, community::all_columns, comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - comment_saved::person_id.nullable().is_not_null(), - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -197,15 +135,7 @@ fn queries<'a>() -> Queries< query = query.filter( community::visibility .ne(CommunityVisibility::Private) - .or(exists( - community_follower::table.filter( - post::community_id.eq(community_follower::community_id).and( - community_follower::person_id - .eq(my_local_user.map(|l| l.person_id).unwrap_or_default()) - .and(community_follower::state.eq(CommunityFollowerState::Accepted)), - ), - ), - )), + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } query.first(&mut conn).await @@ -213,7 +143,6 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (CommentQuery<'a>, &'a Site)| async move { // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); let local_user_id_join = options .local_user .local_user_id() @@ -245,13 +174,7 @@ fn queries<'a>() -> Queries< query = query.filter(post::community_id.eq(community_id)); } - let is_subscribed = exists( - community_follower::table.filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ); + let is_subscribed = community_actions::followed.is_not_null(); match options.listing_type.unwrap_or_default() { ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ @@ -262,29 +185,27 @@ fn queries<'a>() -> Queries< } ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), ListingType::ModeratorView => { - query = query.filter(exists( - community_moderator::table.filter( - post::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id_join)), - ), - )); + query = query.filter(community_actions::became_moderator.is_not_null()); } } // If its saved only, then filter, and order by the saved time, not the comment creation time. if options.saved_only.unwrap_or_default() { query = query - .filter(comment_saved::person_id.is_not_null()) - .then_order_by(comment_saved::published.desc()); + .filter(comment_actions::saved.is_not_null()) + .then_order_by(comment_actions::saved.desc()); } if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); + query = query + .filter(not_creator_filter) + .filter(comment_actions::like_score.eq(1)); } else if options.disliked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); + query = query + .filter(not_creator_filter) + .filter(comment_actions::like_score.eq(-1)); } } @@ -305,21 +226,10 @@ fn queries<'a>() -> Queries< )); // Don't show blocked communities or persons - query = query.filter(not(exists( - instance_block::table.filter( - community::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(exists( - community_block::table.filter( - community::id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(is_creator_blocked(person_id_join))); + query = query + .filter(instance_actions::blocked.is_null()) + .filter(community_actions::blocked.is_null()) + .filter(person_actions::blocked.is_null()); }; if !options.local_user.show_nsfw(site) { @@ -334,15 +244,7 @@ fn queries<'a>() -> Queries< query = query.filter( community::visibility .ne(CommunityVisibility::Private) - .or(exists( - community_follower::table.filter( - post::community_id.eq(community_follower::community_id).and( - community_follower::person_id - .eq(person_id_join) - .and(community_follower::state.eq(CommunityFollowerState::Accepted)), - ), - ), - )), + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } @@ -409,10 +311,10 @@ fn queries<'a>() -> Queries< } impl CommentView { - pub async fn read<'a>( + pub async fn read( pool: &mut DbPool<'_>, comment_id: CommentId, - my_local_user: Option<&'a LocalUser>, + my_local_user: Option<&'_ LocalUser>, ) -> Result { // If a person is given, then my_vote (res.9), if None, should be 0, not null // Necessary to differentiate between other person's votes @@ -446,7 +348,7 @@ pub struct CommentQuery<'a> { pub max_depth: Option, } -impl<'a> CommentQuery<'a> { +impl CommentQuery<'_> { pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { Ok( queries() @@ -1166,6 +1068,8 @@ mod tests { child_count: 5, hot_rank: RANK_DEFAULT, controversy_rank: 0.0, + report_count: 0, + unresolved_report_count: 0, }, }) } diff --git a/crates/db_views/src/local_user_view.rs b/crates/db_views/src/local_user_view.rs index 8d55b96fe..68072cb5a 100644 --- a/crates/db_views/src/local_user_view.rs +++ b/crates/db_views/src/local_user_view.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ ReadFn, }, }; -use lemmy_utils::error::{LemmyError, LemmyErrorType}; +use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; use std::future::{ready, Ready}; enum ReadBy<'a> { @@ -146,7 +146,7 @@ impl LocalUserView { name: &str, bio: &str, admin: bool, - ) -> Result { + ) -> LemmyResult { let instance_id = Instance::read_or_create(pool, "example.com".to_string()) .await? .id; @@ -163,7 +163,9 @@ impl LocalUserView { }; let local_user = LocalUser::create(pool, &user_form, vec![]).await?; - LocalUserView::read(pool, local_user.id).await + LocalUserView::read(pool, local_user.id) + .await + .with_lemmy_type(LemmyErrorType::NotFound) } } diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index d6577af38..9429c258f 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -10,27 +10,23 @@ use diesel::{ }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommunityId, PersonId, PostId, PostReportId}, schema::{ community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, - person_post_aggregates, + person_actions, post, + post_actions, post_aggregates, - post_hide, - post_like, - post_read, post_report, - post_saved, }, source::community::CommunityFollower, utils::{ + actions, + actions_alias, functions::coalesce, get_conn, limit_and_offset, @@ -52,25 +48,16 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(person::table.on(post_report::creator_id.eq(person::id))) .inner_join(aliases::person1.on(post::creator_id.eq(aliases::person1.field(person::id)))) - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post::creator_id)), - ), - ) - .left_join( - aliases::community_moderator1.on( - aliases::community_moderator1 - .field(community_moderator::community_id) - .eq(post::community_id) - .and( - aliases::community_moderator1 - .field(community_moderator::person_id) - .eq(my_person_id), - ), - ), - ) + .left_join(actions_alias( + creator_community_actions, + post::creator_id, + post::community_id, + )) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) .left_join( local_user::table.on( post::creator_id @@ -78,55 +65,12 @@ fn queries<'a>() -> Queries< .and(local_user::admin.eq(true)), ), ) - .left_join( - post_saved::table.on( - post::id - .eq(post_saved::post_id) - .and(post_saved::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_read::table.on( - post::id - .eq(post_read::post_id) - .and(post_read::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_hide::table.on( - post::id - .eq(post_hide::post_id) - .and(post_hide::person_id.eq(my_person_id)), - ), - ) - .left_join( - person_block::table.on( - post::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - person_post_aggregates::table.on( - post::id - .eq(person_post_aggregates::post_id) - .and(person_post_aggregates::person_id.eq(my_person_id)), - ), - ) - .left_join( - community_follower::table.on( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_like::table.on( - post::id - .eq(post_like::post_id) - .and(post_like::person_id.eq(my_person_id)), - ), - ) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + post::creator_id, + )) .inner_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) .left_join( aliases::person2 @@ -138,20 +82,23 @@ fn queries<'a>() -> Queries< community::all_columns, person::all_columns, aliases::person1.fields(person::all_columns), - community_person_ban::community_id.nullable().is_not_null(), - aliases::community_moderator1 - .field(community_moderator::community_id) + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) .nullable() .is_not_null(), local_user::admin.nullable().is_not_null(), CommunityFollower::select_subscribed_type(), - post_saved::post_id.nullable().is_not_null(), - post_read::post_id.nullable().is_not_null(), - post_hide::post_id.nullable().is_not_null(), - person_block::target_id.nullable().is_not_null(), - post_like::score.nullable(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + post_actions::like_score.nullable(), coalesce( - post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(), + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), post_aggregates::all_columns, @@ -195,19 +142,10 @@ fn queries<'a>() -> Queries< // If its not an admin, get only the ones you mod if !user.local_user.admin { - query - .inner_join( - community_moderator::table.on( - community_moderator::community_id - .eq(post::community_id) - .and(community_moderator::person_id.eq(user.person.id)), - ), - ) - .load::(&mut conn) - .await - } else { - query.load::(&mut conn).await + query = query.filter(community_actions::became_moderator.is_not_null()); } + + query.load::(&mut conn).await }; Queries::new(read, list) @@ -247,10 +185,11 @@ impl PostReportView { if !admin { query .inner_join( - community_moderator::table.on( - community_moderator::community_id + community_actions::table.on( + community_actions::community_id .eq(post::community_id) - .and(community_moderator::person_id.eq(my_person_id)), + .and(community_actions::person_id.eq(my_person_id)) + .and(community_actions::became_moderator.is_not_null()), ), ) .select(count(post_report::id)) @@ -293,6 +232,7 @@ mod tests { structs::LocalUserView, }; use lemmy_db_schema::{ + aggregates::structs::PostAggregates, assert_length, source::{ community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, @@ -397,6 +337,10 @@ mod tests { let read_jessica_report_view = PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; + // Make sure the triggers are reading the aggregates correctly. + let agg_1 = PostAggregates::read(pool, inserted_post.id).await?; + let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?; + assert_eq!( read_jessica_report_view.post_report, inserted_jessica_report @@ -407,6 +351,10 @@ mod tests { assert_eq!(read_jessica_report_view.post_creator.id, inserted_timmy.id); assert_eq!(read_jessica_report_view.my_vote, None); assert_eq!(read_jessica_report_view.resolver, None); + assert_eq!(agg_1.report_count, 1); + assert_eq!(agg_1.unresolved_report_count, 1); + assert_eq!(agg_2.report_count, 1); + assert_eq!(agg_2.unresolved_report_count, 1); // Do a batch read of timmys reports let reports = PostReportQuery::default().list(pool, &timmy_view).await?; @@ -440,6 +388,16 @@ mod tests { Some(inserted_timmy.id) ); + // Make sure the unresolved_post report got decremented in the trigger + let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?; + assert_eq!(agg_2.report_count, 1); + assert_eq!(agg_2.unresolved_report_count, 0); + + // Make sure the other unresolved report isn't changed + let agg_1 = PostAggregates::read(pool, inserted_post.id).await?; + assert_eq!(agg_1.report_count, 1); + assert_eq!(agg_1.unresolved_report_count, 1); + // Do a batch read of timmys reports // It should only show saras, which is unresolved let reports_after_resolve = PostReportQuery { diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index e628ac598..0244b9b05 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -5,11 +5,8 @@ use diesel::{ pg::Pg, query_builder::AsQuery, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, OptionalExtension, @@ -20,29 +17,23 @@ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ aggregates::structs::{post_aggregates_keys as key, PostAggregates}, + aliases::creator_community_actions, impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, LocalUserId, PersonId, PostId}, schema::{ community, - community_block, - community_follower, - community_moderator, - community_person_ban, + community_actions, community_post_tag, image_details, - instance_block, + instance_actions, local_user, local_user_language, person, - person_block, - person_post_aggregates, + person_actions, post, + post_actions, post_aggregates, post_community_post_tag, - post_hide, - post_like, - post_read, - post_saved, }, source::{ community::{CommunityFollower, CommunityFollowerState}, @@ -50,6 +41,9 @@ use lemmy_db_schema::{ site::Site, }, utils::{ + action_query, + actions, + actions_alias, functions::coalesce, fuzzy_search, get_conn, @@ -70,36 +64,13 @@ use lemmy_db_schema::{ use tracing::debug; use PostSortType::*; +type QueriesReadTypes<'a> = (PostId, Option<&'a LocalUser>, bool); +type QueriesListTypes<'a> = (PostQuery<'a>, &'a Site); + fn queries<'a>() -> Queries< - impl ReadFn<'a, PostView, (PostId, Option<&'a LocalUser>, bool)>, - impl ListFn<'a, PostView, (PostQuery<'a>, &'a Site)>, + impl ReadFn<'a, PostView, QueriesReadTypes<'a>>, + impl ListFn<'a, PostView, QueriesListTypes<'a>>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - post_aggregates::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post_aggregates::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - post_aggregates::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - post_aggregates::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(post_aggregates::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( post_aggregates::creator_id @@ -108,47 +79,6 @@ fn queries<'a>() -> Queries< ), ); - let is_read = |person_id| { - exists( - post_read::table.filter( - post_aggregates::post_id - .eq(post_read::post_id) - .and(post_read::person_id.eq(person_id)), - ), - ) - }; - - let is_hidden = |person_id| { - exists( - post_hide::table.filter( - post_aggregates::post_id - .eq(post_hide::post_id) - .and(post_hide::person_id.eq(person_id)), - ), - ) - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - post_aggregates::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - post_like::table - .filter( - post_aggregates::post_id - .eq(post_like::post_id) - .and(post_like::person_id.eq(person_id)), - ) - .select(post_like::score.nullable()) - .single_value() - }; - // TODO maybe this should go to localuser also let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option| { @@ -227,7 +157,7 @@ fn queries<'a>() -> Queries< Box::new(None::.into_sql::>()) }; - // We fetch post tags by letting postgresql aggregate them internally in a subquery into JSON. + // We fetch post tags by letting postgresql aggregate them internally in a subquery into JSON. // This is a simple way to join m rows into n rows without duplicating the data and getting // complex diesel types. In pure SQL you would usually do this either using a LEFT JOIN + then // aggregating the results in the application code. But this results in a lot of duplicate @@ -251,37 +181,60 @@ fn queries<'a>() -> Queries< .filter(post_community_post_tag::post_id.eq(post_aggregates::post_id)) .single_value(), ); - query .inner_join(person::table) .inner_join(community::table) .inner_join(post::table) .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) - .left_join( - post_saved::table.on( - post_aggregates::post_id - .eq(post_saved::post_id) - .and(post_saved::person_id.eq(my_person_id.unwrap_or(PersonId(-1)))), - ), - ) + .left_join(actions( + community_actions::table, + my_person_id, + post_aggregates::community_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + post_aggregates::creator_id, + )) + .left_join(actions( + post_actions::table, + my_person_id, + post_aggregates::post_id, + )) + .left_join(actions( + instance_actions::table, + my_person_id, + post_aggregates::instance_id, + )) + .left_join(actions_alias( + creator_community_actions, + post_aggregates::creator_id, + post_aggregates::community_id, + )) .select(( post::all_columns, person::all_columns, community::all_columns, image_details::all_columns.nullable(), - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, post_aggregates::all_columns, - subscribed_type_selection, - post_saved::person_id.nullable().is_not_null(), - is_read_selection, - is_hidden_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + post_actions::like_score.nullable(), coalesce( - post_aggregates::comments.nullable() - read_comments, + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), community_post_tags, @@ -333,17 +286,7 @@ fn queries<'a>() -> Queries< .filter( community::visibility .ne(CommunityVisibility::Private) - .or(exists( - community_follower::table.filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and( - community_follower::person_id - .eq(my_local_user.map(|l| l.person_id).unwrap_or_default()) - .and(community_follower::state.eq(CommunityFollowerState::Accepted)), - ), - ), - )), + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } @@ -357,7 +300,6 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (PostQuery<'a>, &'a Site)| async move { // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); let local_user_id_join = options .local_user .local_user_id() @@ -399,13 +341,7 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::creator_id.eq(creator_id)); } - let is_subscribed = exists( - community_follower::table.filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ); + let is_subscribed = community_actions::followed.is_not_null(); match options.listing_type.unwrap_or_default() { ListingType::Subscribed => query = query.filter(is_subscribed), ListingType::Local => { @@ -415,13 +351,7 @@ fn queries<'a>() -> Queries< } ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), ListingType::ModeratorView => { - query = query.filter(exists( - community_moderator::table.filter( - post::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id_join)), - ), - )); + query = query.filter(community_actions::became_moderator.is_not_null()); } } @@ -462,8 +392,8 @@ fn queries<'a>() -> Queries< // If its saved only, then filter, and order by the saved time, not the comment creation time. if options.saved_only.unwrap_or_default() { query = query - .filter(post_saved::person_id.is_not_null()) - .then_order_by(post_saved::published.desc()); + .filter(post_actions::saved.is_not_null()) + .then_order_by(post_actions::saved.desc()); } // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read // setting wont be able to see saved posts. @@ -473,24 +403,26 @@ fn queries<'a>() -> Queries< { // Do not hide read posts when it is a user profile view // Or, only hide read posts on non-profile views - if let (None, Some(person_id)) = (options.creator_id, options.local_user.person_id()) { - query = query.filter(not(is_read(person_id))); + if options.creator_id.is_none() { + query = query.filter(post_actions::read.is_null()); } } - if !options.show_hidden.unwrap_or_default() { - // If a creator id isn't given (IE its on home or community pages), hide the hidden posts - if let (None, Some(person_id)) = (options.creator_id, options.local_user.person_id()) { - query = query.filter(not(is_hidden(person_id))); - } + // If a creator id isn't given (IE its on home or community pages), hide the hidden posts + if !options.show_hidden.unwrap_or_default() && options.creator_id.is_none() { + query = query.filter(post_actions::hidden.is_null()); } if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = post_aggregates::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); + query = query + .filter(not_creator_filter) + .filter(post_actions::like_score.eq(1)); } else if options.disliked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); + query = query + .filter(not_creator_filter) + .filter(post_actions::like_score.eq(-1)); } }; @@ -500,47 +432,27 @@ fn queries<'a>() -> Queries< query = query.filter( community::visibility .ne(CommunityVisibility::Private) - .or(exists( - community_follower::table.filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)) - .and(community_follower::state.eq(CommunityFollowerState::Accepted)), - ), - )), + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } // Dont filter blocks or missing languages for moderator view type - if let (Some(person_id), false) = ( - options.local_user.person_id(), - options.listing_type.unwrap_or_default() == ListingType::ModeratorView, - ) { - // Filter out the rows with missing languages - query = query.filter(exists( - local_user_language::table.filter( - post::language_id - .eq(local_user_language::language_id) - .and(local_user_language::local_user_id.eq(local_user_id_join)), - ), - )); + if options.listing_type.unwrap_or_default() != ListingType::ModeratorView { + // Filter out the rows with missing languages if user is logged in + if options.local_user.is_some() { + query = query.filter(exists( + local_user_language::table.filter( + post::language_id + .eq(local_user_language::language_id) + .and(local_user_language::local_user_id.eq(local_user_id_join)), + ), + )); + } // Don't show blocked instances, communities or persons - query = query.filter(not(exists( - community_block::table.filter( - post_aggregates::community_id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(exists( - instance_block::table.filter( - post_aggregates::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(is_creator_blocked(person_id))); + query = query.filter(community_actions::blocked.is_null()); + query = query.filter(instance_actions::blocked.is_null()); + query = query.filter(person_actions::blocked.is_null()); } let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -624,10 +536,10 @@ fn queries<'a>() -> Queries< } impl PostView { - pub async fn read<'a>( + pub async fn read( pool: &mut DbPool<'_>, post_id: PostId, - my_local_user: Option<&'a LocalUser>, + my_local_user: Option<&'_ LocalUser>, is_mod_or_admin: bool, ) -> Result { queries() @@ -694,6 +606,7 @@ pub struct PostQuery<'a> { } impl<'a> PostQuery<'a> { + #[allow(clippy::expect_used)] async fn prefetch_upper_bound_for_page_before( &self, site: &Site, @@ -710,13 +623,10 @@ impl<'a> PostQuery<'a> { // covers the "worst case" of the whole page consisting of posts from one community // but using the largest community decreases the pagination-frame so make the real query more // efficient. - use lemmy_db_schema::schema::{ - community_aggregates::dsl::{community_aggregates, community_id, users_active_month}, - community_follower::dsl::{ - community_follower, - community_id as follower_community_id, - person_id, - }, + use lemmy_db_schema::schema::community_aggregates::dsl::{ + community_aggregates, + community_id, + users_active_month, }; let (limit, offset) = limit_and_offset(self.page, self.limit)?; if offset != 0 && self.page_after.is_some() { @@ -727,9 +637,9 @@ impl<'a> PostQuery<'a> { let self_person_id = self.local_user.expect("part of the above if").person_id; let largest_subscribed = { let conn = &mut get_conn(pool).await?; - community_follower - .filter(person_id.eq(self_person_id)) - .inner_join(community_aggregates.on(community_id.eq(follower_community_id))) + action_query(community_actions::followed) + .filter(community_actions::person_id.eq(self_person_id)) + .inner_join(community_aggregates.on(community_id.eq(community_actions::community_id))) .order_by(users_active_month.desc()) .select(community_id) .limit(1) @@ -843,6 +753,7 @@ mod tests { PostLike, PostLikeForm, PostRead, + PostReadForm, PostSaved, PostSavedForm, PostUpdateForm, @@ -850,7 +761,7 @@ mod tests { site::Site, }, traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, - utils::{build_db_pool, build_db_pool_for_tests, get_conn, DbPool, RANK_DEFAULT}, + utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, SubscribedType, @@ -858,10 +769,7 @@ mod tests { use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; - use std::{ - collections::HashSet, - time::{Duration, Instant}, - }; + use std::time::{Duration, Instant}; use url::Url; const POST_WITH_ANOTHER_TITLE: &str = "Another title"; @@ -1266,11 +1174,8 @@ mod tests { let pool = &mut pool.into(); let mut data = init_data(pool).await?; - let post_like_form = PostLikeForm { - post_id: data.inserted_post.id, - person_id: data.local_user_view.person.id, - score: 1, - }; + let post_like_form = + PostLikeForm::new(data.inserted_post.id, data.local_user_view.person.id, 1); let inserted_post_like = PostLike::like(pool, &post_like_form).await?; @@ -1314,7 +1219,7 @@ mod tests { let like_removed = PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id).await?; - assert_eq!(1, like_removed); + assert_eq!(uplete::Count::only_deleted(1), like_removed); cleanup(data, pool).await } @@ -1327,18 +1232,12 @@ mod tests { // 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, - }; + let post_like_form = + PostLikeForm::new(data.inserted_post.id, data.local_user_view.person.id, 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, - }; + let bot_post_like_form = + PostLikeForm::new(data.inserted_bot_post.id, data.local_user_view.person.id, 1); PostLike::like(pool, &bot_post_like_form).await?; // Read the liked only @@ -1376,10 +1275,8 @@ mod tests { // Save only the bot post // The saved_only should only show the bot post - let post_save_form = PostSavedForm { - post_id: data.inserted_bot_post.id, - person_id: data.local_user_view.person.id, - }; + let post_save_form = + PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); PostSaved::save(pool, &post_save_form).await?; // Read the saved only @@ -1815,12 +1712,8 @@ mod tests { data.local_user_view.local_user.show_read_posts = false; // Mark a post as read - PostRead::mark_as_read( - pool, - HashSet::from([data.inserted_bot_post.id]), - data.local_user_view.person.id, - ) - .await?; + let read_form = PostReadForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); + PostRead::mark_as_read(pool, &read_form).await?; // Make sure you don't see the read post in the results let post_listings_hide_read = data.default_post_query().list(&data.site, pool).await?; @@ -1862,7 +1755,7 @@ mod tests { // Mark a post as hidden PostHide::hide( pool, - HashSet::from([data.inserted_bot_post.id]), + data.inserted_bot_post.id, data.local_user_view.person.id, ) .await?; @@ -2060,6 +1953,8 @@ mod tests { community_id: inserted_post.community_id, creator_id: inserted_post.creator_id, instance_id: data.inserted_instance.id, + report_count: 0, + unresolved_report_count: 0, }, subscribed: SubscribedType::NotSubscribed, read: false, diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 0b9a2708a..2286b7dc6 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -12,8 +12,8 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, newtypes::{PersonId, PrivateMessageId}, - schema::{instance_block, person, person_block, private_message}, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + schema::{instance_actions, person, person_actions, private_message}, + utils::{actions, get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, }; use tracing::debug; @@ -27,20 +27,16 @@ fn queries<'a>() -> Queries< .inner_join( aliases::person1.on(private_message::recipient_id.eq(aliases::person1.field(person::id))), ) - .left_join( - person_block::table.on( - private_message::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(aliases::person1.field(person::id))), - ), - ) - .left_join( - instance_block::table.on( - person::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(aliases::person1.field(person::id))), - ), - ) + .left_join(actions( + person_actions::table, + Some(aliases::person1.field(person::id)), + private_message::creator_id, + )) + .left_join(actions( + instance_actions::table, + Some(aliases::person1.field(person::id)), + person::instance_id, + )) }; let selection = ( @@ -62,9 +58,9 @@ fn queries<'a>() -> Queries< let mut query = all_joins(private_message::table.into_boxed()) .select(selection) // Dont show replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) // Dont show replies from blocked instances - .filter(instance_block::person_id.is_null()); + .filter(instance_actions::blocked.is_null()); // If its unread, I only want the ones to me if options.unread_only { @@ -127,24 +123,20 @@ impl PrivateMessageView { private_message::table // Necessary to get the senders instance_id .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .left_join( - person_block::table.on( - private_message::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - instance_block::table.on( - person::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(my_person_id), + private_message::creator_id, + )) + .left_join(actions( + instance_actions::table, + Some(my_person_id), + person::instance_id, + )) // Dont count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) // Dont count replies from blocked instances - .filter(instance_block::person_id.is_null()) + .filter(instance_actions::blocked.is_null()) .filter(private_message::read.eq(false)) .filter(private_message::recipient_id.eq(my_person_id)) .filter(private_message::deleted.eq(false)) diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index b5821ef26..0fa0a5d7e 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -242,6 +242,7 @@ mod tests { enable_animated_images: inserted_sara_local_user.enable_animated_images, enable_private_messages: inserted_sara_local_user.enable_private_messages, collapse_bot_comments: inserted_sara_local_user.collapse_bot_comments, + auto_mark_fetched_posts_as_read: false, }, creator: Person { id: inserted_sara_person.id, diff --git a/crates/db_views/src/vote_view.rs b/crates/db_views/src/vote_view.rs index 79ba7f72a..827cd3cc9 100644 --- a/crates/db_views/src/vote_view.rs +++ b/crates/db_views/src/vote_view.rs @@ -1,17 +1,11 @@ use crate::structs::VoteView; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; +use diesel::{result::Error, ExpressionMethods, NullableExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ + aliases::creator_community_actions, newtypes::{CommentId, PostId}, - schema::{comment, comment_like, community_person_ban, person, post, post_like}, - utils::{get_conn, limit_and_offset, DbPool}, + schema::{comment, comment_actions, community_actions, person, post, post_actions}, + utils::{action_query, actions_alias, get_conn, limit_and_offset, DbPool}, }; impl VoteView { @@ -24,24 +18,24 @@ impl VoteView { let conn = &mut get_conn(pool).await?; let (limit, offset) = limit_and_offset(page, limit)?; - post_like::table + action_query(post_actions::like_score) .inner_join(person::table) .inner_join(post::table) - // Join to community_person_ban to get creator_banned_from_community - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post_like::person_id)), - ), - ) - .filter(post_like::post_id.eq(post_id)) + .left_join(actions_alias( + creator_community_actions, + post_actions::person_id, + post::community_id, + )) + .filter(post_actions::post_id.eq(post_id)) .select(( person::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - post_like::score, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + post_actions::like_score.assume_not_null(), )) - .order_by(post_like::score) + .order_by(post_actions::like_score) .limit(limit) .offset(offset) .load::(conn) @@ -57,25 +51,24 @@ impl VoteView { let conn = &mut get_conn(pool).await?; let (limit, offset) = limit_and_offset(page, limit)?; - comment_like::table + action_query(comment_actions::like_score) .inner_join(person::table) - .inner_join(comment::table) - .inner_join(post::table.on(comment::post_id.eq(post::id))) - // Join to community_person_ban to get creator_banned_from_community - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment_like::person_id)), - ), - ) - .filter(comment_like::comment_id.eq(comment_id)) + .inner_join(comment::table.inner_join(post::table)) + .left_join(actions_alias( + creator_community_actions, + comment_actions::person_id, + post::community_id, + )) + .filter(comment_actions::comment_id.eq(comment_id)) .select(( person::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - comment_like::score, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + comment_actions::like_score.assume_not_null(), )) - .order_by(comment_like::score) + .order_by(comment_actions::like_score) .limit(limit) .offset(offset) .load::(conn) @@ -141,19 +134,11 @@ mod tests { let inserted_comment = Comment::create(pool, &comment_form, None).await?; // Timmy upvotes his own post - let timmy_post_vote_form = PostLikeForm { - post_id: inserted_post.id, - person_id: inserted_timmy.id, - score: 1, - }; + let timmy_post_vote_form = PostLikeForm::new(inserted_post.id, inserted_timmy.id, 1); PostLike::like(pool, &timmy_post_vote_form).await?; // Sara downvotes timmy's post - let sara_post_vote_form = PostLikeForm { - post_id: inserted_post.id, - person_id: inserted_sara.id, - score: -1, - }; + let sara_post_vote_form = PostLikeForm::new(inserted_post.id, inserted_sara.id, -1); PostLike::like(pool, &sara_post_vote_form).await?; let expected_post_vote_views = [ diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 8694298e0..6c5442e6a 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -3,39 +3,40 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommentReplyId, PersonId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, comment_reply, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, post, }, - source::{ - community::{CommunityFollower, CommunityFollowerState}, - local_user::LocalUser, + source::{community::CommunityFollower, local_user::LocalUser}, + utils::{ + actions, + actions_alias, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, }, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, }; @@ -43,74 +44,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, CommentReplyView, (CommentReplyId, Option)>, impl ListFn<'a, CommentReplyView, CommentReplyQuery>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_saved = |person_id| { - exists( - comment_saved::table.filter( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(CommunityFollower::select_subscribed_type()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -121,48 +54,6 @@ fn queries<'a>() -> Queries< let all_joins = move |query: comment_reply::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression< - _, - Pg, - SqlType = sql_types::Nullable, - >, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_saved_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_saved(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(comment::table) .inner_join(person::table.on(comment::creator_id.eq(person::id))) @@ -170,6 +61,22 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(aliases::person1) .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( comment_reply::all_columns, comment::all_columns, @@ -178,14 +85,20 @@ fn queries<'a>() -> Queries< community::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - is_saved_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -228,9 +141,7 @@ fn queries<'a>() -> Queries< }; // Don't show replies from blocked persons - if let Some(my_person_id) = options.my_person_id { - query = query.filter(not(is_creator_blocked(my_person_id))); - } + query = query.filter(person_actions::blocked.is_null()); let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -264,13 +175,11 @@ impl CommentReplyView { let mut query = comment_reply::table .inner_join(comment::table) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(local_user.person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(local_user.person_id), + comment::creator_id, + )) .inner_join(person::table.on(comment::creator_id.eq(person::id))) .into_boxed(); @@ -281,7 +190,7 @@ impl CommentReplyView { query // Don't count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) .filter(comment_reply::recipient_id.eq(local_user.person_id)) .filter(comment_reply::read.eq(false)) .filter(comment::deleted.eq(false)) diff --git a/crates/db_views_actor/src/community_follower_view.rs b/crates/db_views_actor/src/community_follower_view.rs index f9413a078..c32ccb5b8 100644 --- a/crates/db_views_actor/src/community_follower_view.rs +++ b/crates/db_views_actor/src/community_follower_view.rs @@ -12,16 +12,16 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, - schema::{community, community_follower, community_moderator, person}, + schema::{community, community_actions, person}, source::{ community::{Community, CommunityFollower, CommunityFollowerState}, person::Person, }, - utils::{get_conn, limit_and_offset, DbPool}, + utils::{action_query, get_conn, limit_and_offset, DbPool}, CommunityVisibility, SubscribedType, }; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; impl CommunityFollowerView { /// return a list of local community ids and remote inboxes that at least one user of the given @@ -30,7 +30,7 @@ impl CommunityFollowerView { pool: &mut DbPool<'_>, instance_id: InstanceId, published_since: chrono::DateTime, - ) -> Result, Error> { + ) -> LemmyResult> { 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 @@ -39,28 +39,29 @@ impl CommunityFollowerView { // that would work for all instances that support fully shared inboxes. // It would be a bit more complicated though to keep it in sync. - community_follower::table + community_actions::table .inner_join(community::table) - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .filter(person::instance_id.eq(instance_id)) .filter(community::local) // this should be a no-op since community_followers table only has // local-person+remote-community or remote-person+local-community .filter(not(person::local)) - .filter(community_follower::published.gt(published_since.naive_utc())) + .filter(community_actions::followed.gt(published_since.naive_utc())) .select((community::id, person::inbox_url)) .distinct() // only need each community_id, inbox combination once .load::<(CommunityId, DbUrl)>(conn) .await + .with_lemmy_type(LemmyErrorType::NotFound) } pub async fn get_community_follower_inboxes( pool: &mut DbPool<'_>, community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let res = community_follower::table - .filter(community_follower::community_id.eq(community_id)) + let res = action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community_id)) .filter(not(person::local)) - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .select(person::inbox_url) .distinct() .load::(conn) @@ -73,8 +74,8 @@ impl CommunityFollowerView { community_id: CommunityId, ) -> Result { let conn = &mut get_conn(pool).await?; - let res = community_follower::table - .filter(community_follower::community_id.eq(community_id)) + let res = action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community_id)) .select(count_star()) .first::(conn) .await?; @@ -84,11 +85,11 @@ impl CommunityFollowerView { pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_follower::table + action_query(community_actions::followed) .inner_join(community::table) - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .select((community::all_columns, person::all_columns)) - .filter(community_follower::person_id.eq(person_id)) + .filter(community_actions::person_id.eq(person_id)) .filter(community::deleted.eq(false)) .filter(community::removed.eq(false)) .order_by(community::title) @@ -110,7 +111,7 @@ impl CommunityFollowerView { let (limit, offset) = limit_and_offset(page, limit)?; let (person_alias, community_follower_alias) = diesel::alias!( person as person_alias, - community_follower as community_follower_alias + community_actions as community_follower_alias ); // check if the community already has an accepted follower from the same instance @@ -120,7 +121,7 @@ impl CommunityFollowerView { community_follower_alias.on( person_alias .field(person::id) - .eq(community_follower_alias.field(community_follower::person_id)), + .eq(community_follower_alias.field(community_actions::person_id)), ), ) .filter( @@ -128,36 +129,33 @@ impl CommunityFollowerView { .eq(person_alias.field(person::instance_id)) .and( community_follower_alias - .field(community_follower::community_id) - .eq(community_follower::community_id), + .field(community_actions::community_id) + .eq(community_actions::community_id), ) .and( community_follower_alias - .field(community_follower::state) + .field(community_actions::follow_state) .eq(CommunityFollowerState::Accepted), ), ), )); - let mut query = community_follower::table - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + let mut query = action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .inner_join(community::table) .into_boxed(); if all_communities { // if param is false, only return items for communities where user is a mod - query = query.filter(exists( - community_moderator::table.filter( - community_follower::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id)), - ), - )); + query = query + .filter(community_actions::became_moderator.is_not_null()) + .filter(community_actions::person_id.eq(person_id)); } if pending_only { - query = query.filter(community_follower::state.eq(CommunityFollowerState::ApprovalRequired)); + query = + query.filter(community_actions::follow_state.eq(CommunityFollowerState::ApprovalRequired)); } let res = query - .order_by(community_follower::published.asc()) + .order_by(community_actions::followed.asc()) .limit(limit) .offset(offset) .select(( @@ -188,11 +186,11 @@ impl CommunityFollowerView { community_id: CommunityId, ) -> Result { let conn = &mut get_conn(pool).await?; - community_follower::table - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) - .filter(community_follower::community_id.eq(community_id)) - .filter(community_follower::state.eq(CommunityFollowerState::ApprovalRequired)) - .select(count(community_follower::community_id)) + action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .filter(community_actions::community_id.eq(community_id)) + .filter(community_actions::follow_state.eq(CommunityFollowerState::ApprovalRequired)) + .select(count(community_actions::community_id)) .first::(conn) .await } @@ -206,10 +204,10 @@ impl CommunityFollowerView { } let conn = &mut get_conn(pool).await?; select(exists( - community_follower::table - .filter(community_follower::community_id.eq(community.id)) - .filter(community_follower::person_id.eq(from_person_id)) - .filter(community_follower::state.eq(CommunityFollowerState::Accepted)), + action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community.id)) + .filter(community_actions::person_id.eq(from_person_id)) + .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), )) .get_result::(conn) .await? @@ -223,11 +221,30 @@ impl CommunityFollowerView { ) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; select(exists( - community_follower::table - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) - .filter(community_follower::community_id.eq(community_id)) + action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .filter(community_actions::community_id.eq(community_id)) .filter(person::instance_id.eq(instance_id)) - .filter(community_follower::state.eq(CommunityFollowerState::Accepted)), + .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), + )) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(diesel::NotFound) + } + + pub async fn is_follower( + community_id: CommunityId, + instance_id: InstanceId, + pool: &mut DbPool<'_>, + ) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + select(exists( + action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .filter(community_actions::community_id.eq(community_id)) + .filter(person::instance_id.eq(instance_id)) + .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), )) .get_result::(conn) .await? diff --git a/crates/db_views_actor/src/community_moderator_view.rs b/crates/db_views_actor/src/community_moderator_view.rs index 7126af1f6..a9ada92e1 100644 --- a/crates/db_views_actor/src/community_moderator_view.rs +++ b/crates/db_views_actor/src/community_moderator_view.rs @@ -1,12 +1,12 @@ use crate::structs::CommunityModeratorView; -use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl}; +use diesel::{dsl::exists, result::Error, select, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, - schema::{community, community_moderator, person}, + schema::{community, community_actions, person}, source::local_user::LocalUser, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, DbPool}, }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -16,17 +16,11 @@ impl CommunityModeratorView { find_community_id: CommunityId, find_person_id: PersonId, ) -> LemmyResult<()> { - use lemmy_db_schema::schema::community_moderator::dsl::{ - community_id, - community_moderator, - person_id, - }; let conn = &mut get_conn(pool).await?; - select(exists( - community_moderator - .filter(community_id.eq(find_community_id)) - .filter(person_id.eq(find_person_id)), - )) + select(exists(find_action( + community_actions::became_moderator, + (find_person_id, find_community_id), + ))) .get_result::(conn) .await? .then_some(()) @@ -37,10 +31,10 @@ impl CommunityModeratorView { pool: &mut DbPool<'_>, find_person_id: PersonId, ) -> LemmyResult<()> { - use lemmy_db_schema::schema::community_moderator::dsl::{community_moderator, person_id}; let conn = &mut get_conn(pool).await?; select(exists( - community_moderator.filter(person_id.eq(find_person_id)), + action_query(community_actions::became_moderator) + .filter(community_actions::person_id.eq(find_person_id)), )) .get_result::(conn) .await? @@ -53,12 +47,12 @@ impl CommunityModeratorView { community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table + action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) - .filter(community_moderator::community_id.eq(community_id)) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + .filter(community_actions::community_id.eq(community_id)) .select((community::all_columns, person::all_columns)) - .order_by(community_moderator::published) + .order_by(community_actions::became_moderator) .load::(conn) .await } @@ -69,10 +63,10 @@ impl CommunityModeratorView { local_user: Option<&LocalUser>, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let mut query = community_moderator::table + let mut query = action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) - .filter(community_moderator::person_id.eq(person_id)) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + .filter(community_actions::person_id.eq(person_id)) .select((community::all_columns, person::all_columns)) .into_boxed(); @@ -95,16 +89,16 @@ impl CommunityModeratorView { /// Ideally this should be a group by, but diesel doesn't support it yet pub async fn get_community_first_mods(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table + action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) .select((community::all_columns, person::all_columns)) // A hacky workaround instead of group_bys // https://stackoverflow.com/questions/24042359/how-to-join-only-one-row-in-joined-table-with-postgres - .distinct_on(community_moderator::community_id) + .distinct_on(community_actions::community_id) .order_by(( - community_moderator::community_id, - community_moderator::published, + community_actions::community_id, + community_actions::became_moderator, )) .load::(conn) .await diff --git a/crates/db_views_actor/src/community_person_ban_view.rs b/crates/db_views_actor/src/community_person_ban_view.rs index 9bfa0704c..224ea8d53 100644 --- a/crates/db_views_actor/src/community_person_ban_view.rs +++ b/crates/db_views_actor/src/community_person_ban_view.rs @@ -2,14 +2,12 @@ use crate::structs::CommunityPersonBanView; use diesel::{ dsl::{exists, not}, select, - ExpressionMethods, - QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, PersonId}, - schema::community_person_ban, - utils::{get_conn, DbPool}, + schema::community_actions, + utils::{find_action, get_conn, DbPool}, }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -20,11 +18,10 @@ impl CommunityPersonBanView { from_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - community_person_ban::table - .filter(community_person_ban::community_id.eq(from_community_id)) - .filter(community_person_ban::person_id.eq(from_person_id)), - ))) + select(not(exists(find_action( + community_actions::received_ban, + (from_person_id, from_community_id), + )))) .get_result::(conn) .await? .then_some(()) diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 999ec23f0..f6ce82d37 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -4,7 +4,6 @@ use diesel::{ result::Error, BoolExpressionMethods, ExpressionMethods, - JoinOnDsl, NullableExpressionMethods, PgTextExpressionMethods, QueryDsl, @@ -13,20 +12,14 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, - schema::{ - community, - community_aggregates, - community_block, - community_follower, - community_person_ban, - instance_block, - }, + schema::{community, community_actions, community_aggregates, instance_actions}, source::{ community::{CommunityFollower, CommunityFollowerState}, local_user::LocalUser, site::Site, }, utils::{ + actions, functions::lower, fuzzy_search, limit_and_offset, @@ -41,52 +34,34 @@ use lemmy_db_schema::{ }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +type QueriesReadTypes<'a> = (CommunityId, Option<&'a LocalUser>, bool); +type QueriesListTypes<'a> = (CommunityQuery<'a>, &'a Site); + fn queries<'a>() -> Queries< - impl ReadFn<'a, CommunityView, (CommunityId, Option<&'a LocalUser>, bool)>, - impl ListFn<'a, CommunityView, (CommunityQuery<'a>, &'a Site)>, + impl ReadFn<'a, CommunityView, QueriesReadTypes<'a>>, + impl ListFn<'a, CommunityView, QueriesListTypes<'a>>, > { let all_joins = |query: community::BoxedQuery<'a, Pg>, my_local_user: Option<&'a LocalUser>| { - // The left join below will return None in this case - let person_id_join = my_local_user.person_id().unwrap_or(PersonId(-1)); - query .inner_join(community_aggregates::table) - .left_join( - community_follower::table.on( - community::id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ) - .left_join( - instance_block::table.on( - community::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ) - .left_join( - community_block::table.on( - community::id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ) - .left_join( - community_person_ban::table.on( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id_join)), - ), - ) + .left_join(actions( + community_actions::table, + my_local_user.person_id(), + community::id, + )) + .left_join(actions( + instance_actions::table, + my_local_user.person_id(), + community::instance_id, + )) }; let selection = ( community::all_columns, CommunityFollower::select_subscribed_type(), - community_block::community_id.nullable().is_not_null(), + community_actions::blocked.nullable().is_not_null(), community_aggregates::all_columns, - community_person_ban::person_id.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), ); let not_removed_or_deleted = community::removed @@ -118,9 +93,6 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { use CommunitySortType::*; - // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); - let mut query = all_joins(community::table.into_boxed(), options.local_user).select(selection); if let Some(search_term) = options.search_term { @@ -140,7 +112,7 @@ fn queries<'a>() -> Queries< query = query.filter(not_removed_or_deleted).filter( community::hidden .eq(false) - .or(community_follower::person_id.eq(person_id_join)), + .or(community_actions::follow_state.is_not_null()), ); } @@ -168,7 +140,7 @@ fn queries<'a>() -> Queries< if let Some(listing_type) = options.listing_type { query = match listing_type { ListingType::Subscribed => { - query.filter(community_follower::state.eq(CommunityFollowerState::Accepted)) + query.filter(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) } ListingType::Local => query.filter(community::local.eq(true)), _ => query, @@ -177,8 +149,8 @@ fn queries<'a>() -> Queries< // Don't show blocked communities and communities on blocked instances. nsfw communities are // also hidden (based on profile setting) - query = query.filter(instance_block::person_id.is_null()); - query = query.filter(community_block::person_id.is_null()); + query = query.filter(instance_actions::blocked.is_null()); + query = query.filter(community_actions::blocked.is_null()); if !(options.local_user.show_nsfw(site) || options.show_nsfw) { query = query.filter(community::nsfw.eq(false)); } @@ -197,10 +169,10 @@ fn queries<'a>() -> Queries< } impl CommunityView { - pub async fn read<'a>( + pub async fn read( pool: &mut DbPool<'_>, community_id: CommunityId, - my_local_user: Option<&'a LocalUser>, + my_local_user: Option<&'_ LocalUser>, is_mod_or_admin: bool, ) -> Result { queries() @@ -284,7 +256,7 @@ pub struct CommunityQuery<'a> { pub limit: Option, } -impl<'a> CommunityQuery<'a> { +impl CommunityQuery<'_> { pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { queries().list(pool, (self, site)).await } @@ -317,7 +289,7 @@ mod tests { CommunityVisibility, SubscribedType, }; - use lemmy_utils::error::LemmyResult; + use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serial_test::serial; use url::Url; @@ -526,7 +498,7 @@ mod tests { }; let communities = query.list(&data.site, pool).await?; for (i, c) in communities.iter().enumerate().skip(1) { - let prev = communities.get(i - 1).expect("No previous community?"); + let prev = communities.get(i - 1).ok_or(LemmyErrorType::NotFound)?; assert!(c.community.title.cmp(&prev.community.title).is_ge()); } @@ -536,7 +508,7 @@ mod tests { }; let communities = query.list(&data.site, pool).await?; for (i, c) in communities.iter().enumerate().skip(1) { - let prev = communities.get(i - 1).expect("No previous community?"); + let prev = communities.get(i - 1).ok_or(LemmyErrorType::NotFound)?; assert!(c.community.title.cmp(&prev.community.title).is_le()); } diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index 2bc701805..08be67a82 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -3,39 +3,40 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{PersonId, PersonMentionId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, person_mention, post, }, - source::{ - community::{CommunityFollower, CommunityFollowerState}, - local_user::LocalUser, + source::{community::CommunityFollower, local_user::LocalUser}, + utils::{ + actions, + actions_alias, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, }, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, }; @@ -43,74 +44,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, PersonMentionView, (PersonMentionId, Option)>, impl ListFn<'a, PersonMentionView, PersonMentionQuery>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_saved = |person_id| { - exists( - comment_saved::table.filter( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(CommunityFollower::select_subscribed_type()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -121,47 +54,6 @@ fn queries<'a>() -> Queries< let all_joins = move |query: person_mention::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression< - _, - Pg, - SqlType = sql_types::Nullable, - >, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_saved_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_saved(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(comment::table) .inner_join(person::table.on(comment::creator_id.eq(person::id))) @@ -169,6 +61,22 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(aliases::person1) .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( person_mention::all_columns, comment::all_columns, @@ -177,14 +85,20 @@ fn queries<'a>() -> Queries< community::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - is_saved_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -227,9 +141,7 @@ fn queries<'a>() -> Queries< }; // Don't show mentions from blocked persons - if let Some(my_person_id) = options.my_person_id { - query = query.filter(not(is_creator_blocked(my_person_id))); - } + query = query.filter(person_actions::blocked.is_null()); let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -264,13 +176,11 @@ impl PersonMentionView { let mut query = person_mention::table .inner_join(comment::table) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(local_user.person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(local_user.person_id), + comment::creator_id, + )) .inner_join(person::table.on(comment::creator_id.eq(person::id))) .into_boxed(); @@ -281,7 +191,7 @@ impl PersonMentionView { query // Don't count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) .filter(person_mention::recipient_id.eq(local_user.person_id)) .filter(person_mention::read.eq(false)) .filter(comment::deleted.eq(false)) diff --git a/crates/db_views_moderator/src/admin_allow_instance.rs b/crates/db_views_moderator/src/admin_allow_instance.rs new file mode 100644 index 000000000..2a0aaad14 --- /dev/null +++ b/crates/db_views_moderator/src/admin_allow_instance.rs @@ -0,0 +1,52 @@ +use crate::structs::{AdminAllowInstanceView, ModlogListParams}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::PersonId, + schema::{admin_allow_instance, instance, person}, + utils::{get_conn, limit_and_offset, DbPool}, +}; + +impl AdminAllowInstanceView { + pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + + let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); + let show_mod_names = !params.hide_modlog_names; + let show_mod_names_expr = show_mod_names.as_sql::(); + + let admin_names_join = admin_allow_instance::admin_person_id + .eq(person::id) + .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); + let mut query = admin_allow_instance::table + .left_join(person::table.on(admin_names_join)) + .inner_join(instance::table) + .select(( + admin_allow_instance::all_columns, + instance::all_columns, + person::all_columns.nullable(), + )) + .into_boxed(); + + if let Some(admin_person_id) = params.mod_person_id { + query = query.filter(admin_allow_instance::admin_person_id.eq(admin_person_id)); + }; + + let (limit, offset) = limit_and_offset(params.page, params.limit)?; + + query + .limit(limit) + .offset(offset) + .order_by(admin_allow_instance::when_.desc()) + .load::(conn) + .await + } +} diff --git a/crates/db_views_moderator/src/admin_block_instance.rs b/crates/db_views_moderator/src/admin_block_instance.rs new file mode 100644 index 000000000..e9d7c8b0d --- /dev/null +++ b/crates/db_views_moderator/src/admin_block_instance.rs @@ -0,0 +1,52 @@ +use crate::structs::{AdminBlockInstanceView, ModlogListParams}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::PersonId, + schema::{admin_block_instance, instance, person}, + utils::{get_conn, limit_and_offset, DbPool}, +}; + +impl AdminBlockInstanceView { + pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + + let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); + let show_mod_names = !params.hide_modlog_names; + let show_mod_names_expr = show_mod_names.as_sql::(); + + let admin_names_join = admin_block_instance::admin_person_id + .eq(person::id) + .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); + let mut query = admin_block_instance::table + .left_join(person::table.on(admin_names_join)) + .inner_join(instance::table) + .select(( + admin_block_instance::all_columns, + instance::all_columns, + person::all_columns.nullable(), + )) + .into_boxed(); + + if let Some(admin_person_id) = params.mod_person_id { + query = query.filter(admin_block_instance::admin_person_id.eq(admin_person_id)); + }; + + let (limit, offset) = limit_and_offset(params.page, params.limit)?; + + query + .limit(limit) + .offset(offset) + .order_by(admin_block_instance::when_.desc()) + .load::(conn) + .await + } +} diff --git a/crates/db_views_moderator/src/lib.rs b/crates/db_views_moderator/src/lib.rs index d3e7efffd..5748707c6 100644 --- a/crates/db_views_moderator/src/lib.rs +++ b/crates/db_views_moderator/src/lib.rs @@ -1,4 +1,8 @@ #[cfg(feature = "full")] +pub mod admin_allow_instance; +#[cfg(feature = "full")] +pub mod admin_block_instance; +#[cfg(feature = "full")] pub mod admin_purge_comment_view; #[cfg(feature = "full")] pub mod admin_purge_community_view; diff --git a/crates/db_views_moderator/src/structs.rs b/crates/db_views_moderator/src/structs.rs index 27ee82522..06e9f099a 100644 --- a/crates/db_views_moderator/src/structs.rs +++ b/crates/db_views_moderator/src/structs.rs @@ -5,22 +5,29 @@ use lemmy_db_schema::{ source::{ comment::Comment, community::Community, - moderator::{ - AdminPurgeComment, - AdminPurgeCommunity, - AdminPurgePerson, - AdminPurgePost, - ModAdd, - ModAddCommunity, - ModBan, - ModBanFromCommunity, - ModFeaturePost, - ModHideCommunity, - ModLockPost, - ModRemoveComment, - ModRemoveCommunity, - ModRemovePost, - ModTransferCommunity, + instance::Instance, + mod_log::{ + admin::{ + AdminAllowInstance, + AdminBlockInstance, + AdminPurgeComment, + AdminPurgeCommunity, + AdminPurgePerson, + AdminPurgePost, + }, + moderator::{ + ModAdd, + ModAddCommunity, + ModBan, + ModBanFromCommunity, + ModFeaturePost, + ModHideCommunity, + ModLockPost, + ModRemoveComment, + ModRemoveCommunity, + ModRemovePost, + ModTransferCommunity, + }, }, person::Person, post::Post, @@ -233,6 +240,32 @@ pub struct AdminPurgePostView { pub community: Community, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a post. +pub struct AdminBlockInstanceView { + pub admin_block_instance: AdminBlockInstance, + pub instance: Instance, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a post. +pub struct AdminAllowInstanceView { + pub admin_block_instance: AdminAllowInstance, + pub instance: Instance, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[cfg_attr(feature = "full", derive(TS, Queryable))] diff --git a/crates/federate/src/inboxes.rs b/crates/federate/src/inboxes.rs index 1649e019f..ec96b1d6c 100644 --- a/crates/federate/src/inboxes.rs +++ b/crates/federate/src/inboxes.rs @@ -1,5 +1,4 @@ use crate::util::LEMMY_TEST_FAST_FEDERATION; -use anyhow::Result; use async_trait::async_trait; use chrono::{DateTime, TimeZone, Utc}; use lemmy_db_schema::{ @@ -8,6 +7,7 @@ use lemmy_db_schema::{ utils::{ActualDbPool, DbPool}, }; use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_utils::error::LemmyResult; use reqwest::Url; use std::{ collections::{HashMap, HashSet}, @@ -23,6 +23,7 @@ use std::{ /// currently fairly high because of the current structure of storing inboxes for every person, not /// having a separate list of shared_inboxes, and the architecture of having every instance queue be /// fully separate. (see https://github.com/LemmyNet/lemmy/issues/3958) +#[allow(clippy::expect_used)] static FOLLOW_ADDITIONS_RECHECK_DELAY: LazyLock = LazyLock::new(|| { if *LEMMY_TEST_FAST_FEDERATION { chrono::TimeDelta::try_seconds(1).expect("TimeDelta out of bounds") @@ -33,20 +34,18 @@ static FOLLOW_ADDITIONS_RECHECK_DELAY: LazyLock = LazyLock::n /// The same as FOLLOW_ADDITIONS_RECHECK_DELAY, but triggering when the last person on an instance /// unfollows a specific remote community. This is expected to happen pretty rarely and updating it /// in a timely manner is not too important. +#[allow(clippy::expect_used)] static FOLLOW_REMOVALS_RECHECK_DELAY: LazyLock = LazyLock::new(|| chrono::TimeDelta::try_hours(1).expect("TimeDelta out of bounds")); #[async_trait] pub trait DataSource: Send + Sync { - async fn read_site_from_instance_id( - &self, - instance_id: InstanceId, - ) -> Result, diesel::result::Error>; + async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> LemmyResult; async fn get_instance_followed_community_inboxes( &self, instance_id: InstanceId, last_fetch: DateTime, - ) -> Result, diesel::result::Error>; + ) -> LemmyResult>; } pub struct DbDataSource { pool: ActualDbPool, @@ -60,10 +59,7 @@ impl DbDataSource { #[async_trait] impl DataSource for DbDataSource { - async fn read_site_from_instance_id( - &self, - instance_id: InstanceId, - ) -> Result, diesel::result::Error> { + async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> LemmyResult { Site::read_from_instance_id(&mut DbPool::Pool(&self.pool), instance_id).await } @@ -71,7 +67,7 @@ impl DataSource for DbDataSource { &self, instance_id: InstanceId, last_fetch: DateTime, - ) -> Result, diesel::result::Error> { + ) -> LemmyResult> { CommunityFollowerView::get_instance_followed_community_inboxes( &mut DbPool::Pool(&self.pool), instance_id, @@ -126,7 +122,7 @@ impl CommunityInboxCollector { /// most often this will return 0 values (if instance doesn't care about the activity) /// or 1 value (the shared inbox) /// > 1 values only happens for non-lemmy software - pub async fn get_inbox_urls(&mut self, activity: &SentActivity) -> Result> { + pub async fn get_inbox_urls(&mut self, activity: &SentActivity) -> LemmyResult> { let mut inbox_urls: HashSet = HashSet::new(); if activity.send_all_instances { @@ -134,7 +130,8 @@ impl CommunityInboxCollector { self.site = self .data_source .read_site_from_instance_id(self.instance_id) - .await?; + .await + .ok(); self.site_loaded = true; } if let Some(site) = &self.site { @@ -168,7 +165,7 @@ impl CommunityInboxCollector { Ok(inbox_urls.into_iter().collect()) } - pub async fn update_communities(&mut self) -> Result<()> { + pub async fn update_communities(&mut self) -> LemmyResult<()> { if (Utc::now() - self.last_full_communities_fetch) > *FOLLOW_REMOVALS_RECHECK_DELAY { tracing::debug!("{}: fetching full list of communities", self.domain); // process removals every hour @@ -201,7 +198,7 @@ impl CommunityInboxCollector { &mut self, instance_id: InstanceId, last_fetch: DateTime, - ) -> Result<(HashMap>, DateTime)> { + ) -> LemmyResult<(HashMap>, DateTime)> { // update to time before fetch to ensure overlap. subtract some time to ensure overlap even if // published date is not exact let new_last_fetch = Utc::now() - *FOLLOW_ADDITIONS_RECHECK_DELAY / 2; @@ -236,12 +233,12 @@ mod tests { DataSource {} #[async_trait] impl DataSource for DataSource { - async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> Result, diesel::result::Error>; + async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> LemmyResult; async fn get_instance_followed_community_inboxes( &self, instance_id: InstanceId, last_fetch: DateTime, - ) -> Result, diesel::result::Error>; + ) -> LemmyResult>; } } @@ -299,7 +296,7 @@ mod tests { collector .data_source .expect_read_site_from_instance_id() - .return_once(move |_| Ok(Some(site))); + .return_once(move |_| Ok(site)); let activity = SentActivity { id: ActivityId(1), @@ -333,14 +330,8 @@ mod tests { .expect_get_instance_followed_community_inboxes() .return_once(move |_, _| { Ok(vec![ - ( - community_id, - Url::parse(url1).map_err(|_| diesel::NotFound)?.into(), - ), - ( - community_id, - Url::parse(url2).map_err(|_| diesel::NotFound)?.into(), - ), + (community_id, Url::parse(url1)?.into()), + (community_id, Url::parse(url2)?.into()), ]) }); @@ -428,20 +419,13 @@ mod tests { collector .data_source .expect_read_site_from_instance_id() - .return_once(move |_| Ok(Some(site))); + .return_once(move |_| Ok(site)); let subdomain_inbox = "https://follower.example.com/inbox"; collector .data_source .expect_get_instance_followed_community_inboxes() - .return_once(move |_, _| { - Ok(vec![( - community_id, - Url::parse(subdomain_inbox) - .map_err(|_| diesel::NotFound)? - .into(), - )]) - }); + .return_once(move |_, _| Ok(vec![(community_id, Url::parse(subdomain_inbox)?.into())])); collector.update_communities().await?; let user1_inbox = Url::parse("https://example.com/user1/inbox")?; @@ -472,6 +456,7 @@ mod tests { Ok(()) } + #[allow(clippy::expect_used)] #[tokio::test] async fn test_update_communities() -> LemmyResult<()> { let mut collector = setup_collector(); @@ -493,26 +478,11 @@ mod tests { .returning(move |_, last_fetch| { if last_fetch == Utc.timestamp_nanos(0) { Ok(vec![ - ( - community_id1, - Url::parse(user1_inbox_str) - .map_err(|_| diesel::NotFound)? - .into(), - ), - ( - community_id2, - Url::parse(user2_inbox_str) - .map_err(|_| diesel::NotFound)? - .into(), - ), + (community_id1, Url::parse(user1_inbox_str)?.into()), + (community_id2, Url::parse(user2_inbox_str)?.into()), ]) } else { - Ok(vec![( - community_id3, - Url::parse(user3_inbox_str) - .map_err(|_| diesel::NotFound)? - .into(), - )]) + Ok(vec![(community_id3, Url::parse(user3_inbox_str)?.into())]) } }); @@ -566,7 +536,7 @@ mod tests { collector .data_source .expect_read_site_from_instance_id() - .return_once(move |_| Ok(Some(site))); + .return_once(move |_| Ok(site)); collector .data_source diff --git a/crates/federate/src/lib.rs b/crates/federate/src/lib.rs index 983749de3..dbb92949e 100644 --- a/crates/federate/src/lib.rs +++ b/crates/federate/src/lib.rs @@ -199,10 +199,14 @@ mod test { use super::*; use activitypub_federation::config::Data; use chrono::DateTime; - use lemmy_db_schema::source::{ - federation_allowlist::FederationAllowList, - federation_blocklist::FederationBlockList, - instance::InstanceForm, + use lemmy_db_schema::{ + source::{ + federation_allowlist::{FederationAllowList, FederationAllowListForm}, + federation_blocklist::{FederationBlockList, FederationBlockListForm}, + instance::InstanceForm, + person::{Person, PersonInsertForm}, + }, + traits::Crud, }; use lemmy_utils::error::LemmyError; use serial_test::serial; @@ -318,14 +322,22 @@ mod test { async fn test_send_manager_blocked() -> LemmyResult<()> { let mut data = TestData::init(1, 1).await?; - let domain = data.instances[0].domain.clone(); - FederationBlockList::replace(&mut data.context.pool(), Some(vec![domain])).await?; + let instance_id = data.instances[0].id; + let form = PersonInsertForm::new("tim".to_string(), String::new(), instance_id); + let person = Person::create(&mut data.context.pool(), &form).await?; + let form = FederationBlockListForm { + instance_id, + updated: None, + expires: None, + }; + FederationBlockList::block(&mut data.context.pool(), &form).await?; data.run().await?; let workers = &data.send_manager.workers; assert_eq!(2, workers.len()); assert!(workers.contains_key(&data.instances[1].id)); assert!(workers.contains_key(&data.instances[2].id)); + Person::delete(&mut data.context.pool(), person.id).await?; data.cleanup().await?; Ok(()) } @@ -336,13 +348,20 @@ mod test { async fn test_send_manager_allowed() -> LemmyResult<()> { let mut data = TestData::init(1, 1).await?; - let domain = data.instances[0].domain.clone(); - FederationAllowList::replace(&mut data.context.pool(), Some(vec![domain])).await?; + let instance_id = data.instances[0].id; + let form = PersonInsertForm::new("tim".to_string(), String::new(), instance_id); + let person = Person::create(&mut data.context.pool(), &form).await?; + let form = FederationAllowListForm { + instance_id: data.instances[0].id, + updated: None, + }; + FederationAllowList::allow(&mut data.context.pool(), &form).await?; data.run().await?; let workers = &data.send_manager.workers; assert_eq!(1, workers.len()); assert!(workers.contains_key(&data.instances[0].id)); + Person::delete(&mut data.context.pool(), person.id).await?; data.cleanup().await?; Ok(()) } diff --git a/crates/federate/src/send.rs b/crates/federate/src/send.rs index 01d620eb0..d77ebe9f7 100644 --- a/crates/federate/src/send.rs +++ b/crates/federate/src/send.rs @@ -84,7 +84,7 @@ pub(crate) struct SendRetryTask<'a> { pub stop: CancellationToken, } -impl<'a> SendRetryTask<'a> { +impl SendRetryTask<'_> { // this function will return successfully when (a) send succeeded or (b) worker cancelled // and will return an error if an internal error occurred (send errors cause an infinite loop) pub async fn send_retry_loop(self) -> Result<()> { diff --git a/crates/federate/src/worker.rs b/crates/federate/src/worker.rs index b0254ba0b..047c5a5d6 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -22,8 +22,9 @@ use lemmy_db_schema::{ federation_queue_state::FederationQueueState, instance::{Instance, InstanceForm}, }, - utils::{naive_now, ActualDbPool, DbPool}, + utils::{ActualDbPool, DbPool}, }; +use lemmy_utils::error::LemmyResult; use std::{collections::BinaryHeap, ops::Add, time::Duration}; use tokio::{ sync::mpsc::{self, UnboundedSender}, @@ -86,7 +87,7 @@ impl InstanceWorker { federation_worker_config: FederationWorkerConfig, stop: CancellationToken, stats_sender: UnboundedSender, - ) -> Result<(), anyhow::Error> { + ) -> LemmyResult<()> { let pool = config.to_request_data().inner_pool().clone(); let state = FederationQueueState::load(&mut DbPool::Pool(&pool), instance.id).await?; let (report_send_result, receive_send_result) = @@ -116,7 +117,7 @@ impl InstanceWorker { /// loop fetch new activities from db and send them to the inboxes of the given instances /// this worker only returns if (a) there is an internal error or (b) the cancellation token is /// cancelled (graceful exit) - async fn loop_until_stopped(&mut self) -> Result<()> { + async fn loop_until_stopped(&mut self) -> LemmyResult<()> { self.initial_fail_sleep().await?; let (mut last_sent_id, mut newest_id) = self.get_latest_ids().await?; @@ -149,12 +150,15 @@ impl InstanceWorker { }); // compare to next id based on incrementing if expected_next_id != Some(next_id_to_send.0) { - anyhow::bail!( - "{}: next id to send is not as expected: {:?} != {:?}", - self.instance.domain, - expected_next_id, - next_id_to_send - ) + return Err( + anyhow::anyhow!( + "{}: next id to send is not as expected: {:?} != {:?}", + self.instance.domain, + expected_next_id, + next_id_to_send + ) + .into(), + ); } } @@ -291,7 +295,7 @@ impl InstanceWorker { self.instance.updated = Some(Utc::now()); let form = InstanceForm { - updated: Some(naive_now()), + updated: Some(Utc::now()), ..InstanceForm::new(self.instance.domain.clone()) }; Instance::update(&mut self.pool(), self.instance.id, form).await?; @@ -331,7 +335,7 @@ impl InstanceWorker { self.state.last_successful_published_time = next.published; } - let save_state_every = chrono::Duration::from_std(SAVE_STATE_EVERY_TIME).expect("not negative"); + let save_state_every = chrono::Duration::from_std(SAVE_STATE_EVERY_TIME)?; if force_write || (Utc::now() - self.last_state_insert) > save_state_every { self.save_and_send_state().await?; } @@ -341,7 +345,7 @@ impl InstanceWorker { /// we collect the relevant inboxes in the main instance worker task, and only spawn the send task /// if we have inboxes to send to this limits CPU usage and reduces overhead for the (many) /// cases where we don't have any inboxes - async fn spawn_send_if_needed(&mut self, activity_id: ActivityId) -> Result<()> { + async fn spawn_send_if_needed(&mut self, activity_id: ActivityId) -> LemmyResult<()> { let Some(ele) = get_activity_cached(&mut self.pool(), activity_id) .await .context("failed reading activity from db")? @@ -357,11 +361,7 @@ impl InstanceWorker { return Ok(()); }; let activity = &ele.0; - let inbox_urls = self - .inbox_collector - .get_inbox_urls(activity) - .await - .context("failed figuring out inbox urls")?; + let inbox_urls = self.inbox_collector.get_inbox_urls(activity).await?; if inbox_urls.is_empty() { // this is the case when the activity is not relevant to this receiving instance (e.g. no user // subscribed to the relevant community) diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index 4a8c53dea..91c3ed683 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -33,4 +33,4 @@ url = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } http.workspace = true -rss = "2.0.9" +rss = "2.0.10" diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 00518032d..55e9cc7f3 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -23,7 +23,7 @@ use lemmy_db_views_actor::{ use lemmy_utils::{ cache_header::cache_1hour, error::{LemmyError, LemmyErrorType, LemmyResult}, - utils::markdown::{markdown_to_html, sanitize_html}, + utils::markdown::markdown_to_html, }; use rss::{ extension::{dublincore::DublinCoreExtension, ExtensionBuilder, ExtensionMap}, @@ -93,23 +93,6 @@ static RSS_NAMESPACE: LazyLock> = LazyLock::new(|| { h }); -/// Removes any characters disallowed by the XML grammar. -/// See https://www.w3.org/TR/xml/#NT-Char for details. -fn sanitize_xml(input: String) -> String { - input - .chars() - .filter(|&c| { - matches!(c, - '\u{09}' - | '\u{0A}' - | '\u{0D}' - | '\u{20}'..='\u{D7FF}' - | '\u{E000}'..='\u{FFFD}' - | '\u{10000}'..='\u{10FFFF}') - }) - .collect() -} - #[tracing::instrument(skip_all)] async fn get_all_feed( info: web::Query, @@ -278,7 +261,7 @@ async fn get_feed_user( let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?; let channel = Channel { namespaces: RSS_NAMESPACE.clone(), - title: format!("{} - {}", sanitize_xml(site_view.site.name), person.name), + title: format!("{} - {}", site_view.site.name, person.name), link: person.actor_id.to_string(), items, ..Default::default() @@ -319,7 +302,7 @@ async fn get_feed_community( let mut channel = Channel { namespaces: RSS_NAMESPACE.clone(), - title: format!("{} - {}", sanitize_xml(site_view.site.name), community.name), + title: format!("{} - {}", site_view.site.name, community.name), link: community.actor_id.to_string(), items, ..Default::default() @@ -360,7 +343,7 @@ async fn get_feed_front( let items = create_post_items(posts, &protocol_and_hostname)?; let mut channel = Channel { namespaces: RSS_NAMESPACE.clone(), - title: format!("{} - Subscribed", sanitize_xml(site_view.site.name)), + title: format!("{} - Subscribed", site_view.site.name), link: protocol_and_hostname, items, ..Default::default() @@ -411,7 +394,7 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult, protocol_and_hostname: &str) -> Lemmy for p in posts { let post_url = format!("{}/post/{}", protocol_and_hostname, p.post.id); - let community_url = format!( - "{}/c/{}", - protocol_and_hostname, - sanitize_html(&p.community.name) - ); + let community_url = format!("{}/c/{}", protocol_and_hostname, &p.community.name); let dublin_core_ext = Some(DublinCoreExtension { creators: vec![p.creator.actor_id.to_string()], ..DublinCoreExtension::default() @@ -513,9 +492,9 @@ fn create_post_items(posts: Vec, protocol_and_hostname: &str) -> Lemmy }); let mut description = format!("submitted by {} to {}
{} points | {} comments", p.creator.actor_id, - sanitize_html(&p.creator.name), + &p.creator.name, community_url, - sanitize_html(&p.community.name), + &p.community.name, p.counts.score, post_url, p.counts.comments); @@ -566,11 +545,11 @@ fn create_post_items(posts: Vec, protocol_and_hostname: &str) -> Lemmy }; let i = Item { - title: Some(sanitize_html(sanitize_xml(p.post.name).as_str())), + title: Some(p.post.name), pub_date: Some(p.post.published.to_rfc2822()), comments: Some(post_url.clone()), guid, - description: Some(sanitize_xml(description)), + description: Some(description), dublin_core_ext, link: Some(post_url.clone()), extensions, diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index a0f804b6b..50897b95d 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -1,13 +1,14 @@ use actix_web::{ - body::BodyStream, + body::{BodyStream, BoxBody}, http::{ header::{HeaderName, ACCEPT_ENCODING, HOST}, Method, StatusCode, }, - web::{self, Query}, + web::*, HttpRequest, HttpResponse, + Responder, }; use futures::stream::{Stream, StreamExt}; use http::HeaderValue; @@ -24,21 +25,18 @@ use serde::Deserialize; use std::time::Duration; use url::Url; -pub fn config( - cfg: &mut web::ServiceConfig, - client: ClientWithMiddleware, - rate_limit: &RateLimitCell, -) { +pub fn config(cfg: &mut ServiceConfig, client: ClientWithMiddleware, rate_limit: &RateLimitCell) { cfg - .app_data(web::Data::new(client)) + .app_data(Data::new(client)) .service( - web::resource("/pictrs/image") + resource("/pictrs/image") .wrap(rate_limit.image()) - .route(web::post().to(upload)), + .route(post().to(upload)), ) // This has optional query params: /image/{filename}?format=jpg&thumbnail=256 - .service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res))) - .service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete))); + .service(resource("/pictrs/image/{filename}").route(get().to(full_res))) + .service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete))) + .service(resource("/pictrs/healthz").route(get().to(healthz))); } trait ProcessUrl { @@ -128,11 +126,11 @@ fn adapt_request( async fn upload( req: HttpRequest, - body: web::Payload, + body: Payload, // require login local_user_view: LocalUserView, - client: web::Data, - context: web::Data, + client: Data, + context: Data, ) -> LemmyResult { // TODO: check rate limit here let pictrs_config = context.settings().pictrs_config()?; @@ -172,11 +170,11 @@ async fn upload( } async fn full_res( - filename: web::Path, - web::Query(params): web::Query, + filename: Path, + Query(params): Query, req: HttpRequest, - client: web::Data, - context: web::Data, + client: Data, + context: Data, local_user_view: Option, ) -> LemmyResult { // block access to images if instance is private and unauthorized, public @@ -225,10 +223,10 @@ async fn image( } async fn delete( - components: web::Path<(String, String)>, + components: Path<(String, String)>, req: HttpRequest, - client: web::Data, - context: web::Data, + client: Data, + context: Data, // require login _local_user_view: LocalUserView, ) -> LemmyResult { @@ -250,12 +248,31 @@ async fn delete( Ok(HttpResponse::build(convert_status(res.status())).body(BodyStream::new(res.bytes_stream()))) } +async fn healthz( + req: HttpRequest, + client: Data, + context: Data, +) -> LemmyResult { + let pictrs_config = context.settings().pictrs_config()?; + let url = format!("{}healthz", pictrs_config.url); + + let mut client_req = adapt_request(&req, &client, url); + + if let Some(addr) = req.head().peer_addr { + client_req = client_req.header("X-Forwarded-For", addr.to_string()); + } + + let res = client_req.send().await?; + + Ok(HttpResponse::build(convert_status(res.status())).body(BodyStream::new(res.bytes_stream()))) +} + pub async fn image_proxy( Query(params): Query, req: HttpRequest, - client: web::Data, - context: web::Data, -) -> LemmyResult { + client: Data, + context: Data, +) -> LemmyResult, HttpResponse>> { let url = Url::parse(¶ms.url)?; // Check that url corresponds to a federated image so that this can't be abused as a proxy @@ -263,10 +280,19 @@ pub async fn image_proxy( RemoteImage::validate(&mut context.pool(), url.clone().into()).await?; let pictrs_config = context.settings().pictrs_config()?; - let processed_url = params.process_url(¶ms.url, &pictrs_config.url); - image(processed_url, req, &client).await + let bypass_proxy = pictrs_config + .proxy_bypass_domains + .iter() + .any(|s| url.domain().is_some_and(|d| d == s)); + if bypass_proxy { + // Bypass proxy and redirect user to original image + Ok(Either::Left(Redirect::to(url.to_string()).respond_to(&req))) + } else { + // Proxy the image data through Lemmy + Ok(Either::Right(image(processed_url, req, &client).await?)) + } } fn make_send(mut stream: S) -> impl Stream + Send + Unpin + 'static @@ -312,12 +338,16 @@ where } // TODO: remove these conversions after actix-web upgrades to http 1.0 +#[allow(clippy::expect_used)] fn convert_status(status: http::StatusCode) -> StatusCode { StatusCode::from_u16(status.as_u16()).expect("status can be converted") } + +#[allow(clippy::expect_used)] fn convert_method(method: &Method) -> http::Method { http::Method::from_bytes(method.as_str().as_bytes()).expect("method can be converted") } + fn convert_header<'a>(name: &'a http::HeaderName, value: &'a HeaderValue) -> (&'a str, &'a [u8]) { (name.as_str(), value.as_bytes()) } diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs index c5b7024cd..a20a786f9 100644 --- a/crates/routes/src/webfinger.rs +++ b/crates/routes/src/webfinger.rs @@ -3,13 +3,16 @@ use activitypub_federation::{ fetch::webfinger::{extract_webfinger_name, Webfinger, WebfingerLink, WEBFINGER_CONTENT_TYPE}, }; use actix_web::{web, web::Query, HttpResponse}; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{context::LemmyContext, LemmyErrorType}; use lemmy_db_schema::{ source::{community::Community, person::Person}, traits::ApubActor, CommunityVisibility, }; -use lemmy_utils::{cache_header::cache_3days, error::LemmyResult}; +use lemmy_utils::{ + cache_header::cache_3days, + error::{LemmyErrorExt, LemmyResult}, +}; use serde::Deserialize; use std::collections::HashMap; use url::Url; @@ -41,7 +44,7 @@ async fn get_webfinger_response( let links = if name == context.settings().hostname { // webfinger response for instance actor (required for mastodon authorized fetch) let url = Url::parse(&context.settings().get_protocol_and_hostname())?; - vec![webfinger_link_for_actor(Some(url), "none", &context)] + vec![webfinger_link_for_actor(Some(url), "none", &context)?] } else { // webfinger response for user/community let user_id: Option = Person::read_from_name(&mut context.pool(), name, false) @@ -65,8 +68,8 @@ 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 doesn't matter. vec![ - webfinger_link_for_actor(user_id, "Person", &context), - webfinger_link_for_actor(community_id, "Group", &context), + webfinger_link_for_actor(user_id, "Person", &context)?, + webfinger_link_for_actor(community_id, "Group", &context)?, ] } .into_iter() @@ -94,11 +97,11 @@ fn webfinger_link_for_actor( url: Option, kind: &str, context: &LemmyContext, -) -> Vec { +) -> LemmyResult> { if let Some(url) = url { let type_key = "https://www.w3.org/ns/activitystreams#type" .parse() - .expect("parse url"); + .with_lemmy_type(LemmyErrorType::InvalidUrl)?; let mut vec = vec![ WebfingerLink { @@ -128,8 +131,8 @@ fn webfinger_link_for_actor( ..Default::default() }); } - vec + Ok(vec) } else { - vec![] + Ok(vec![]) } } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index c22f863c1..7ed4c0476 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -23,30 +23,31 @@ workspace = true [features] full = [ - "dep:ts-rs", - "dep:diesel", - "dep:rosetta-i18n", - "dep:actix-web", - "dep:reqwest-middleware", - "dep:tracing", - "dep:actix-web", - "dep:serde_json", - "dep:anyhow", - "dep:http", - "dep:deser-hjson", - "dep:regex", - "dep:urlencoding", - "dep:doku", - "dep:url", - "dep:smart-default", - "dep:enum-map", - "dep:futures", - "dep:tokio", - "dep:html2text", - "dep:lettre", - "dep:uuid", - "dep:itertools", - "dep:markdown-it", + "ts-rs", + "diesel", + "rosetta-i18n", + "actix-web", + "reqwest-middleware", + "tracing", + "actix-web", + "serde_json", + "anyhow", + "http", + "deser-hjson", + "regex", + "urlencoding", + "doku", + "url", + "smart-default", + "enum-map", + "futures", + "tokio", + "html2text", + "lettre", + "uuid", + "itertools", + "markdown-it", + "moka", ] [package.metadata.cargo-shear] @@ -71,10 +72,10 @@ uuid = { workspace = true, features = ["serde", "v4"], optional = true } rosetta-i18n = { workspace = true, optional = true } tokio = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true } -html2text = { version = "0.12.5", optional = true } +html2text = { version = "0.12.6", optional = true } deser-hjson = { version = "2.2.4", optional = true } smart-default = { version = "0.7.1", optional = true } -lettre = { version = "0.11.8", default-features = false, features = [ +lettre = { version = "0.11.10", default-features = false, features = [ "builder", "tokio1", "tokio1-rustls-tls", @@ -89,6 +90,7 @@ markdown-it-block-spoiler = "1.0.0" markdown-it-sub = "1.0.0" markdown-it-sup = "1.0.0" markdown-it-ruby = "1.0.0" +moka = { workspace = true, optional = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/crates/utils/src/email.rs b/crates/utils/src/email.rs index 7bac7ad67..46abb47ea 100644 --- a/crates/utils/src/email.rs +++ b/crates/utils/src/email.rs @@ -33,7 +33,7 @@ pub async fn send_email( let email_and_port = email_config.smtp_server.split(':').collect::>(); let email = *email_and_port .first() - .ok_or(LemmyErrorType::MissingAnEmail)?; + .ok_or(LemmyErrorType::EmailRequired)?; let port = email_and_port .get(1) .ok_or(LemmyErrorType::EmailSmtpServerNeedsAPort)? @@ -45,16 +45,20 @@ pub async fn send_email( // use usize::MAX as the line wrap length, since lettre handles the wrapping for us let plain_text = html2text::from_read(html.as_bytes(), usize::MAX); + let smtp_from_address = &email_config.smtp_from_address; + let email = Message::builder() .from( - email_config - .smtp_from_address + smtp_from_address .parse() - .expect("email from address isn't valid"), + .with_lemmy_type(LemmyErrorType::InvalidEmailAddress( + smtp_from_address.into(), + ))?, ) .to(Mailbox::new( Some(to_username.to_string()), - Address::from_str(to_email).expect("email to address isn't valid"), + Address::from_str(to_email) + .with_lemmy_type(LemmyErrorType::InvalidEmailAddress(to_email.into()))?, )) .message_id(Some(format!("<{}@{}>", Uuid::new_v4(), settings.hostname))) .subject(subject) @@ -62,7 +66,7 @@ pub async fn send_email( plain_text, html.to_string(), )) - .expect("email built incorrectly"); + .with_lemmy_type(LemmyErrorType::EmailSendFailed)?; // don't worry about 'dangeous'. it's just that leaving it at the default configuration // is bad. diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index d52df2f72..40f878747 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -73,7 +73,7 @@ pub enum LemmyErrorType { NoEmailSetup, LocalSiteNotSetup, EmailSmtpServerNeedsAPort, - MissingAnEmail, + InvalidEmailAddress(String), RateLimitError, InvalidName, InvalidDisplayName, @@ -129,6 +129,7 @@ pub enum LemmyErrorType { InvalidRegex, CaptchaIncorrect, CouldntCreateAudioCaptcha, + CouldntCreateImageCaptcha, InvalidUrlScheme, CouldntSendWebmention, ContradictingFilters, @@ -150,6 +151,7 @@ pub enum LemmyErrorType { CommunityHasNoFollowers, PostScheduleTimeMustBeInFuture, TooManyScheduledPosts, + CannotCombineFederationBlocklistAndAllowlist, FederationError { #[cfg_attr(feature = "full", ts(optional))] error: Option, @@ -185,6 +187,7 @@ pub enum FederationError { CantDeleteSite, ObjectIsNotPublic, ObjectIsNotPrivate, + Unreachable, } cfg_if! { @@ -276,6 +279,12 @@ cfg_if! { } } + impl From for LemmyErrorType { + fn from(error: FederationError) -> Self { + LemmyErrorType::FederationError { error: Some(error) } + } + } + pub trait LemmyErrorExt> { fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult; } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 1e0cbefbf..3367c91bb 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -42,7 +42,10 @@ macro_rules! location_info { }; } -#[cfg(feature = "full")] +cfg_if! { + if #[cfg(feature = "full")] { +use moka::future::Cache;use std::fmt::Debug;use std::hash::Hash; + /// tokio::spawn, but accepts a future that may fail and also /// * logs errors /// * attaches the spawned task to the tracing span of the caller for better logging @@ -60,3 +63,20 @@ pub fn spawn_try_task( * spawn was called */ ); } + +pub fn build_cache() -> Cache +where + K: Debug + Eq + Hash + Send + Sync + 'static, + V: Debug + Clone + Send + Sync + 'static, +{ + Cache::::builder() + .max_capacity(1) + .time_to_live(CACHE_DURATION_API) + .build() +} + +#[cfg(feature = "full")] +pub type CacheLock = std::sync::LazyLock>; + + } +} diff --git a/crates/utils/src/rate_limit/mod.rs b/crates/utils/src/rate_limit/mod.rs index a6cf92150..ace775287 100644 --- a/crates/utils/src/rate_limit/mod.rs +++ b/crates/utils/src/rate_limit/mod.rs @@ -29,6 +29,7 @@ pub struct RateLimitCell { state: Arc>, } +#[allow(clippy::expect_used)] impl RateLimitCell { pub fn new(rate_limit_config: EnumMap) -> Self { let state = Arc::new(Mutex::new(RateLimitState::new(rate_limit_config))); @@ -133,6 +134,7 @@ pub struct RateLimitedMiddleware { service: Rc, } +#[allow(clippy::expect_used)] impl RateLimitChecker { /// Returns true if the request passed the rate limit, false if it failed and should be rejected. pub fn check(self, ip_addr: IpAddr) -> bool { diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index 01d379986..68f248d6c 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -18,6 +18,7 @@ pub struct InstantSecs { secs: u32, } +#[allow(clippy::expect_used)] impl InstantSecs { pub fn now() -> Self { InstantSecs { diff --git a/crates/utils/src/request.rs b/crates/utils/src/request.rs index e98e0e8a5..5353edb4e 100644 --- a/crates/utils/src/request.rs +++ b/crates/utils/src/request.rs @@ -10,6 +10,7 @@ where } #[tracing::instrument(skip_all)] +#[allow(clippy::expect_used)] async fn retry_custom(f: F) -> Result where F: Fn() -> Fut, diff --git a/crates/utils/src/response.rs b/crates/utils/src/response.rs index f37c15dd7..51ea7198d 100644 --- a/crates/utils/src/response.rs +++ b/crates/utils/src/response.rs @@ -11,19 +11,20 @@ pub fn jsonify_plain_text_errors( return Ok(ErrorHandlerResponse::Response(res.map_into_left_body())); } // We're assuming that any LemmyError is already in JSON format, so we don't need to do anything - if maybe_error - .expect("http responses with 400-599 statuses should have an error object") - .as_error::() - .is_some() - { - return Ok(ErrorHandlerResponse::Response(res.map_into_left_body())); + if let Some(maybe_error) = maybe_error { + if maybe_error.as_error::().is_some() { + return Ok(ErrorHandlerResponse::Response(res.map_into_left_body())); + } } - let (req, res) = res.into_parts(); - let error = res - .error() - .expect("expected an error object in the response"); - let response = HttpResponse::build(res.status()).json(LemmyErrorType::Unknown(error.to_string())); + let (req, res_parts) = res.into_parts(); + let lemmy_err_type = if let Some(error) = res_parts.error() { + LemmyErrorType::Unknown(error.to_string()) + } else { + LemmyErrorType::Unknown("couldnt build json".into()) + }; + + let response = HttpResponse::build(res_parts.status()).json(lemmy_err_type); let service_response = ServiceResponse::new(req, response); Ok(ErrorHandlerResponse::Response( diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index aba1a4fb1..986c19059 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -3,6 +3,7 @@ use anyhow::{anyhow, Context}; use deser_hjson::from_str; use regex::Regex; use std::{env, fs, io::Error, sync::LazyLock}; +use url::Url; use urlencoding::encode; pub mod structs; @@ -11,6 +12,7 @@ use structs::{DatabaseConnection, PictrsConfig, PictrsImageMode, Settings}; static DEFAULT_CONFIG_FILE: &str = "config/config.hjson"; +#[allow(clippy::expect_used)] pub static SETTINGS: LazyLock = LazyLock::new(|| { if env::var("LEMMY_INITIALIZE_WITH_DEFAULT_SETTINGS").is_ok() { println!( @@ -23,6 +25,7 @@ pub static SETTINGS: LazyLock = LazyLock::new(|| { } }); +#[allow(clippy::expect_used)] static WEBFINGER_REGEX: LazyLock = LazyLock::new(|| { Regex::new(&format!( "^acct:([a-zA-Z0-9_]{{3,}})@{}$", @@ -128,3 +131,9 @@ impl PictrsConfig { } } } + +#[allow(clippy::expect_used)] +/// Necessary to avoid URL expect failures +fn pictrs_placeholder_url() -> Url { + Url::parse("http://localhost:8080").expect("parse pictrs url") +} diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index c95f66644..ccc0da45b 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -1,3 +1,4 @@ +use super::pictrs_placeholder_url; use doku::Document; use serde::{Deserialize, Serialize}; use smart_default::SmartDefault; @@ -52,7 +53,7 @@ pub struct Settings { /// Sets a response Access-Control-Allow-Origin CORS header /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin #[default(None)] - #[doku(example = "*")] + #[doku(example = "lemmy.tld")] cors_origin: Option, } @@ -68,7 +69,7 @@ impl Settings { #[serde(default, deny_unknown_fields)] pub struct PictrsConfig { /// Address where pictrs is available (for image hosting) - #[default(Url::parse("http://localhost:8080").expect("parse pictrs url"))] + #[default(pictrs_placeholder_url())] #[doku(example = "http://localhost:8080")] pub url: Url, @@ -87,6 +88,15 @@ pub struct PictrsConfig { #[default(PictrsImageMode::StoreLinkPreviews)] pub(super) image_mode: PictrsImageMode, + /// Allows bypassing proxy for specific image hosts when using ProxyAllImages. + /// + /// imgur.com is bypassed by default to avoid rate limit errors. When specifying any bypass + /// in the config, this default is ignored and you need to list imgur explicitly. To proxy imgur + /// requests, specify a noop bypass list, eg `proxy_bypass_domains ["example.org"]`. + #[default(vec!["i.imgur.com".to_string()])] + #[doku(example = "i.imgur.com")] + pub proxy_bypass_domains: Vec, + /// Timeout for uploading images to pictrs (in seconds) #[default(30)] pub upload_timeout: u64, diff --git a/crates/utils/src/utils/markdown/image_links.rs b/crates/utils/src/utils/markdown/image_links.rs index 7456190e4..9dcea8da7 100644 --- a/crates/utils/src/utils/markdown/image_links.rs +++ b/crates/utils/src/utils/markdown/image_links.rs @@ -58,11 +58,13 @@ fn find_urls(src: &str) -> Vec<(usize, usize)> { let mut links_offsets = vec![]; ast.walk(|node, _depth| { if let Some(image) = node.cast::() { - let (_, node_offset) = node.srcmap.expect("srcmap is none").get_byte_offsets(); - let start_offset = node_offset - image.url_len() - 1 - image.title_len(); - let end_offset = node_offset - 1; + if let Some(srcmap) = node.srcmap { + let (_, node_offset) = srcmap.get_byte_offsets(); + let start_offset = node_offset - image.url_len() - 1 - image.title_len(); + let end_offset = node_offset - 1; - links_offsets.push((start_offset, end_offset)); + links_offsets.push((start_offset, end_offset)); + } } }); links_offsets diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs index 9d34e8a69..3dfa8e9f1 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -259,6 +259,11 @@ mod tests { fn test_sanitize_html() { let sanitized = sanitize_html(" hello &\"'"); let expected = "<script>alert('xss');</script> hello &"'"; - assert_eq!(expected, sanitized) + assert_eq!(expected, sanitized); + + let sanitized = + sanitize_html("Polling the group: what do y'all know about the Orion browser from Kagi?"); + let expected = "Polling the group: what do y'all know about the Orion browser from Kagi?"; + assert_eq!(expected, sanitized); } } diff --git a/crates/utils/src/utils/mention.rs b/crates/utils/src/utils/mention.rs index 13762ed27..0a011f848 100644 --- a/crates/utils/src/utils/mention.rs +++ b/crates/utils/src/utils/mention.rs @@ -2,6 +2,7 @@ use itertools::Itertools; use regex::Regex; use std::sync::LazyLock; +#[allow(clippy::expect_used)] static MENTIONS_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"@(?P[\w.]+)@(?P[a-zA-Z0-9._:-]+)").expect("compile regex") }); diff --git a/crates/utils/src/utils/slurs.rs b/crates/utils/src/utils/slurs.rs index 2350822eb..8df7bc3d3 100644 --- a/crates/utils/src/utils/slurs.rs +++ b/crates/utils/src/utils/slurs.rs @@ -1,8 +1,8 @@ use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use regex::{Regex, RegexBuilder}; -pub fn remove_slurs(test: &str, slur_regex: &Option) -> String { - if let Some(slur_regex) = slur_regex { +pub fn remove_slurs(test: &str, slur_regex: &Option>) -> String { + if let Some(Ok(slur_regex)) = slur_regex { slur_regex.replace_all(test, "*removed*").to_string() } else { test.to_string() @@ -11,9 +11,9 @@ pub fn remove_slurs(test: &str, slur_regex: &Option) -> String { pub(crate) fn slur_check<'a>( test: &'a str, - slur_regex: &'a Option, + slur_regex: &'a Option>, ) -> Result<(), Vec<&'a str>> { - if let Some(slur_regex) = slur_regex { + if let Some(Ok(slur_regex)) = slur_regex { let mut matches: Vec<&str> = slur_regex.find_iter(test).map(|mat| mat.as_str()).collect(); // Unique @@ -30,16 +30,16 @@ pub(crate) fn slur_check<'a>( } } -pub fn build_slur_regex(regex_str: Option<&str>) -> Option { +pub fn build_slur_regex(regex_str: Option<&str>) -> Option> { regex_str.map(|slurs| { RegexBuilder::new(slurs) .case_insensitive(true) .build() - .expect("compile regex") + .with_lemmy_type(LemmyErrorType::InvalidRegex) }) } -pub fn check_slurs(text: &str, slur_regex: &Option) -> LemmyResult<()> { +pub fn check_slurs(text: &str, slur_regex: &Option>) -> LemmyResult<()> { if let Err(slurs) = slur_check(text, slur_regex) { Err(anyhow::anyhow!("{}", slurs_vec_to_str(&slurs))).with_lemmy_type(LemmyErrorType::Slurs) } else { @@ -47,7 +47,10 @@ pub fn check_slurs(text: &str, slur_regex: &Option) -> LemmyResult<()> { } } -pub fn check_slurs_opt(text: &Option, slur_regex: &Option) -> LemmyResult<()> { +pub fn check_slurs_opt( + text: &Option, + slur_regex: &Option>, +) -> LemmyResult<()> { match text { Some(t) => check_slurs(t, slur_regex), None => Ok(()), @@ -64,7 +67,7 @@ pub(crate) fn slurs_vec_to_str(slurs: &[&str]) -> String { mod test { use crate::{ - error::LemmyResult, + error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str}, }; use pretty_assertions::assert_eq; @@ -72,7 +75,7 @@ mod test { #[test] fn test_slur_filter() -> LemmyResult<()> { - let slur_regex = Some(RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build()?); + let slur_regex = Some(RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().with_lemmy_type(LemmyErrorType::InvalidRegex)); let test = "faggot test kike tranny cocksucker retardeds. Capitalized Niggerz. This is a bunch of other safe text."; let slur_free = "No slurs here"; diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index f8da6f609..bc4155e6b 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -6,11 +6,13 @@ use std::sync::LazyLock; use url::{ParseError, Url}; // From here: https://github.com/vector-im/element-android/blob/develop/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt#L35 +#[allow(clippy::expect_used)] static VALID_MATRIX_ID_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^@[A-Za-z0-9\x21-\x39\x3B-\x7F]+:[A-Za-z0-9.-]+(:[0-9]{2,5})?$") .expect("compile regex") }); // taken from https://en.wikipedia.org/wiki/UTM_parameters +#[allow(clippy::expect_used)] static URL_CLEANER: LazyLock = LazyLock::new(|| UrlCleaner::from_embedded_rules().expect("compile clearurls")); const ALLOWED_POST_URL_SCHEMES: [&str; 3] = ["http", "https", "magnet"]; @@ -85,26 +87,20 @@ fn has_newline(name: &str) -> bool { } pub fn is_valid_actor_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> { - static VALID_ACTOR_NAME_REGEX_EN: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex")); - static VALID_ACTOR_NAME_REGEX_AR: LazyLock = - LazyLock::new(|| Regex::new(r"^[\p{Arabic}0-9_]{3,}$").expect("compile regex")); - static VALID_ACTOR_NAME_REGEX_RU: LazyLock = - LazyLock::new(|| Regex::new(r"^[\p{Cyrillic}0-9_]{3,}$").expect("compile regex")); - - let check = name.chars().count() <= actor_name_max_length && !has_newline(name); - // Only allow characters from a single alphabet per username. This avoids problems with lookalike // characters like `o` which looks identical in Latin and Cyrillic, and can be used to imitate // other users. Checks for additional alphabets can be added in the same way. - let lang_check = VALID_ACTOR_NAME_REGEX_EN.is_match(name) - || VALID_ACTOR_NAME_REGEX_AR.is_match(name) - || VALID_ACTOR_NAME_REGEX_RU.is_match(name); + #[allow(clippy::expect_used)] + static VALID_ACTOR_NAME_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^(?:[a-zA-Z0-9_]+|[0-9_\p{Arabic}]+|[0-9_\p{Cyrillic}]+)$").expect("compile regex") + }); - if !check || !lang_check { - Err(LemmyErrorType::InvalidName.into()) - } else { + min_length_check(name, 3, LemmyErrorType::InvalidName)?; + max_length_check(name, actor_name_max_length, LemmyErrorType::InvalidName)?; + if VALID_ACTOR_NAME_REGEX.is_match(name) { Ok(()) + } else { + Err(LemmyErrorType::InvalidName.into()) } } @@ -225,33 +221,32 @@ fn min_length_check(item: &str, min_length: usize, min_msg: LemmyErrorType) -> L } /// Attempts to build a regex and check it for common errors before inserting into the DB. -pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> LemmyResult> { - regex_str_opt.map_or_else( - || Ok(None::), - |regex_str| { - if regex_str.is_empty() { - // If the proposed regex is empty, return as having no regex at all; this is the same - // behavior that happens downstream before the write to the database. - return Ok(None::); - } - - RegexBuilder::new(regex_str) - .case_insensitive(true) - .build() - .with_lemmy_type(LemmyErrorType::InvalidRegex) - .and_then(|regex| { - // NOTE: It is difficult to know, in the universe of user-crafted regex, which ones - // may match against any string text. To keep it simple, we'll match the regex - // against an innocuous string - a single number - which should help catch a regex - // that accidentally matches against all strings. - if regex.is_match("1") { - Err(LemmyErrorType::PermissiveRegex.into()) - } else { - Ok(Some(regex)) - } - }) - }, - ) +pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> Option> { + if let Some(regex) = regex_str_opt { + if regex.is_empty() { + None + } else { + Some( + RegexBuilder::new(regex) + .case_insensitive(true) + .build() + .with_lemmy_type(LemmyErrorType::InvalidRegex) + .and_then(|regex| { + // NOTE: It is difficult to know, in the universe of user-crafted regex, which ones + // may match against any string text. To keep it simple, we'll match the regex + // against an innocuous string - a single number - which should help catch a regex + // that accidentally matches against all strings. + if regex.is_match("1") { + Err(LemmyErrorType::PermissiveRegex.into()) + } else { + Ok(regex) + } + }), + ) + } + } else { + None + } } /// Cleans a url of tracking parameters. @@ -334,7 +329,7 @@ pub fn build_url_str_without_scheme(url_str: &str) -> LemmyResult { // Set the scheme to http, then remove the http:// part url .set_scheme("http") - .map_err(|_| LemmyErrorType::InvalidUrl)?; + .map_err(|_e| LemmyErrorType::InvalidUrl)?; let mut out = url .to_string() @@ -379,11 +374,14 @@ mod tests { use pretty_assertions::assert_eq; use url::Url; + const URL_WITH_TRACKING: &str = "https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123"; + const URL_TRACKING_REMOVED: &str = "https://example.com/path/123?user+name=random+user&id=123"; + #[test] fn test_clean_url_params() -> LemmyResult<()> { - let url = Url::parse("https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123")?; + let url = Url::parse(URL_WITH_TRACKING)?; let cleaned = clean_url(&url); - let expected = Url::parse("https://example.com/path/123?user+name=random+user&id=123")?; + let expected = Url::parse(URL_TRACKING_REMOVED)?; assert_eq!(expected.to_string(), cleaned.to_string()); let url = Url::parse("https://example.com/path/123")?; @@ -395,9 +393,9 @@ mod tests { #[test] fn test_clean_body() -> LemmyResult<()> { - let text = "[a link](https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123)"; - let cleaned = clean_urls_in_text(text); - let expected = "[a link](https://example.com/path/123?user+name=random+user&id=123)"; + let text = format!("[a link]({URL_WITH_TRACKING})"); + let cleaned = clean_urls_in_text(&text); + let expected = format!("[a link]({URL_TRACKING_REMOVED})"); assert_eq!(expected.to_string(), cleaned.to_string()); let text = "[a link](https://example.com/path/123)"; @@ -438,6 +436,15 @@ mod tests { assert!(is_valid_actor_name("a", actor_name_max_length).is_err()); // empty assert!(is_valid_actor_name("", actor_name_max_length).is_err()); + // newline + assert!(is_valid_actor_name( + r"Line1 + +Line3", + actor_name_max_length + ) + .is_err()); + assert!(is_valid_actor_name("Line1\nLine3", actor_name_max_length).is_err()); } #[test] @@ -560,13 +567,27 @@ mod tests { #[test] fn test_valid_slur_regex() { - let valid_regexes = [&None, &Some(""), &Some("(foo|bar)")]; + let valid_regex = Some("(foo|bar)"); + let result = build_and_check_regex(&valid_regex); + assert!( + result.is_some_and(|x| x.is_ok()), + "Testing regex: {:?}", + valid_regex + ); + } - valid_regexes.iter().for_each(|regex| { - let result = build_and_check_regex(regex); + #[test] + fn test_missing_slur_regex() { + let missing_regex = None; + let result = build_and_check_regex(&missing_regex); + assert!(result.is_none()); + } - assert!(result.is_ok(), "Testing regex: {:?}", regex); - }); + #[test] + fn test_empty_slur_regex() { + let empty = Some(""); + let result = build_and_check_regex(&empty); + assert!(result.is_none()); } #[test] @@ -582,9 +603,9 @@ mod tests { .for_each(|(regex_str, expected_err)| { let result = build_and_check_regex(regex_str); - assert!(result.is_err()); + assert!(result.as_ref().is_some_and(Result::is_err)); assert!( - result.is_err_and(|e| e.error_type.eq(&expected_err.clone())), + result.is_some_and(|x| x.is_err_and(|e| e.error_type.eq(&expected_err.clone()))), "Testing regex {:?}, expected error {}", regex_str, expected_err diff --git a/crates/utils/translations b/crates/utils/translations index 7adddded5..dbb09b078 160000 --- a/crates/utils/translations +++ b/crates/utils/translations @@ -1 +1 @@ -Subproject commit 7adddded581fcd965ab33b91c5fe10e0d7247208 +Subproject commit dbb09b0784982827d5d9b7dcf39f1703c1212b83 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c1d8d359b..cb438af3a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,7 +23,7 @@ services: lemmy: # use "image" to pull down an already compiled lemmy. make sure to comment out "build". - # image: dessalines/lemmy:0.19.5 + # image: dessalines/lemmy:0.19.6 # platform: linux/x86_64 # no arm64 support. uncomment platform if using m1. # use "build" to build your local lemmy server image for development. make sure to comment out "image". # run: docker compose up --build @@ -31,9 +31,9 @@ services: build: context: ../ dockerfile: docker/Dockerfile - # args: - # RUST_RELEASE_MODE: release - # CARGO_BUILD_FEATURES: default + # args: + # RUST_RELEASE_MODE: release + # CARGO_BUILD_FEATURES: default # this hostname is used in nginx reverse proxy and also for lemmy ui to connect to the backend, do not change hostname: lemmy restart: unless-stopped @@ -53,7 +53,7 @@ services: lemmy-ui: # use "image" to pull down an already compiled lemmy-ui. make sure to comment out "build". - image: dessalines/lemmy-ui:0.19.5 + image: dessalines/lemmy-ui:0.19.6 # platform: linux/x86_64 # no arm64 support. uncomment platform if using m1. # use "build" to build your local lemmy ui image for development. make sure to comment out "image". # run: docker compose up --build diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 090ad71a8..bc4b5ea7f 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.7" x-ui-default: &ui-default init: true - image: dessalines/lemmy-ui:0.19.5 + image: dessalines/lemmy-ui:0.19.6 # assuming lemmy-ui is cloned besides lemmy directory # build: # context: ../../../lemmy-ui diff --git a/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/down.sql b/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/down.sql new file mode 100644 index 000000000..0f2ff0569 --- /dev/null +++ b/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + DROP COLUMN auto_mark_fetched_posts_as_read; + diff --git a/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/up.sql b/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/up.sql new file mode 100644 index 000000000..fbe57276b --- /dev/null +++ b/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + ADD COLUMN auto_mark_fetched_posts_as_read boolean DEFAULT FALSE NOT NULL; + diff --git a/migrations/2024-11-10-134311_smoosh-tables-together/down.sql b/migrations/2024-11-10-134311_smoosh-tables-together/down.sql new file mode 100644 index 000000000..29b95d2cd --- /dev/null +++ b/migrations/2024-11-10-134311_smoosh-tables-together/down.sql @@ -0,0 +1,320 @@ +-- For each new actions table, create tables that are dropped in up.sql, and insert into them +CREATE TABLE comment_saved ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + comment_id int REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, comment_id) +); + +INSERT INTO comment_saved (person_id, comment_id, published) +SELECT + person_id, + comment_id, + saved +FROM + comment_actions +WHERE + saved IS NOT NULL; + +CREATE TABLE community_block ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_block (person_id, community_id, published) +SELECT + person_id, + community_id, + blocked +FROM + community_actions +WHERE + blocked IS NOT NULL; + +CREATE TABLE community_person_ban ( + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + expires timestamptz, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_person_ban (community_id, person_id, published, expires) +SELECT + community_id, + person_id, + received_ban, + ban_expires +FROM + community_actions +WHERE + received_ban IS NOT NULL; + +CREATE TABLE community_moderator ( + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_moderator (community_id, person_id, published) +SELECT + community_id, + person_id, + became_moderator +FROM + community_actions +WHERE + became_moderator IS NOT NULL; + +CREATE TABLE person_block ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + target_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, target_id) +); + +INSERT INTO person_block (person_id, target_id, published) +SELECT + person_id, + target_id, + blocked +FROM + person_actions +WHERE + blocked IS NOT NULL; + +CREATE TABLE person_post_aggregates ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + read_comments bigint DEFAULT 0 NOT NULL, + published timestamptz NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO person_post_aggregates (person_id, post_id, read_comments, published) +SELECT + person_id, + post_id, + read_comments_amount, + read_comments +FROM + post_actions +WHERE + read_comments IS NOT NULL; + +CREATE TABLE post_hide ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_hide (post_id, person_id, published) +SELECT + post_id, + person_id, + hidden +FROM + post_actions +WHERE + hidden IS NOT NULL; + +CREATE TABLE post_like ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + score smallint NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_like (post_id, person_id, score, published) +SELECT + post_id, + person_id, + like_score, + liked +FROM + post_actions +WHERE + liked IS NOT NULL; + +CREATE TABLE post_saved ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_saved (post_id, person_id, published) +SELECT + post_id, + person_id, + saved +FROM + post_actions +WHERE + saved IS NOT NULL; + +-- Do the opposite of the `ALTER TABLE` commands in up.sql +DELETE FROM comment_actions +WHERE liked IS NULL; + +DELETE FROM community_actions +WHERE followed IS NULL; + +DELETE FROM instance_actions +WHERE blocked IS NULL; + +DELETE FROM person_actions +WHERE followed IS NULL; + +DELETE FROM post_actions +WHERE read IS NULL; + +ALTER TABLE comment_actions RENAME TO comment_like; + +ALTER TABLE community_actions RENAME TO community_follower; + +ALTER TABLE instance_actions RENAME TO instance_block; + +ALTER TABLE person_actions RENAME TO person_follower; + +ALTER TABLE post_actions RENAME TO post_read; + +ALTER TABLE comment_like RENAME COLUMN liked TO published; + +ALTER TABLE comment_like RENAME COLUMN like_score TO score; + +ALTER TABLE community_follower RENAME COLUMN followed TO published; + +ALTER TABLE community_follower RENAME COLUMN follow_state TO state; + +ALTER TABLE community_follower RENAME COLUMN follow_approver_id TO approver_id; + +ALTER TABLE instance_block RENAME COLUMN blocked TO published; + +ALTER TABLE person_follower RENAME COLUMN person_id TO follower_id; + +ALTER TABLE person_follower RENAME COLUMN target_id TO person_id; + +ALTER TABLE person_follower RENAME COLUMN followed TO published; + +ALTER TABLE person_follower RENAME COLUMN follow_pending TO pending; + +ALTER TABLE post_read RENAME COLUMN read TO published; + +ALTER TABLE comment_like + DROP CONSTRAINT comment_actions_check_liked, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN score SET NOT NULL, + DROP COLUMN saved; + +ALTER TABLE community_follower + DROP CONSTRAINT community_actions_check_followed, + DROP CONSTRAINT community_actions_check_received_ban, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN state SET NOT NULL, + DROP COLUMN blocked, + DROP COLUMN became_moderator, + DROP COLUMN received_ban, + DROP COLUMN ban_expires; + +ALTER TABLE instance_block + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(); + +ALTER TABLE person_follower + DROP CONSTRAINT person_actions_check_followed, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN pending SET NOT NULL, + DROP COLUMN blocked; + +ALTER TABLE post_read + DROP CONSTRAINT post_actions_check_read_comments, + DROP CONSTRAINT post_actions_check_liked, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + DROP COLUMN read_comments, + DROP COLUMN read_comments_amount, + DROP COLUMN saved, + DROP COLUMN liked, + DROP COLUMN like_score, + DROP COLUMN hidden; + +-- Rename associated stuff +ALTER INDEX comment_actions_pkey RENAME TO comment_like_pkey; + +ALTER INDEX idx_comment_actions_comment RENAME TO idx_comment_like_comment; + +ALTER TABLE comment_like RENAME CONSTRAINT comment_actions_comment_id_fkey TO comment_like_comment_id_fkey; + +ALTER TABLE comment_like RENAME CONSTRAINT comment_actions_person_id_fkey TO comment_like_person_id_fkey; + +ALTER INDEX community_actions_pkey RENAME TO community_follower_pkey; + +ALTER INDEX idx_community_actions_community RENAME TO idx_community_follower_community; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_community_id_fkey TO community_follower_community_id_fkey; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_person_id_fkey TO community_follower_person_id_fkey; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_follow_approver_id_fkey TO community_follower_approver_id_fkey; + +ALTER INDEX instance_actions_pkey RENAME TO instance_block_pkey; + +ALTER TABLE instance_block RENAME CONSTRAINT instance_actions_instance_id_fkey TO instance_block_instance_id_fkey; + +ALTER TABLE instance_block RENAME CONSTRAINT instance_actions_person_id_fkey TO instance_block_person_id_fkey; + +ALTER INDEX person_actions_pkey RENAME TO person_follower_pkey; + +ALTER TABLE person_follower RENAME CONSTRAINT person_actions_target_id_fkey TO person_follower_person_id_fkey; + +ALTER TABLE person_follower RENAME CONSTRAINT person_actions_person_id_fkey TO person_follower_follower_id_fkey; + +ALTER INDEX post_actions_pkey RENAME TO post_read_pkey; + +ALTER TABLE post_read RENAME CONSTRAINT post_actions_person_id_fkey TO post_read_person_id_fkey; + +ALTER TABLE post_read RENAME CONSTRAINT post_actions_post_id_fkey TO post_read_post_id_fkey; + +-- Rename idx_community_actions_followed and remove filter +CREATE INDEX idx_community_follower_published ON community_follower (published); + +DROP INDEX idx_community_actions_followed; + +-- Move indexes back to their original tables +CREATE INDEX idx_comment_saved_comment ON comment_saved (comment_id); + +CREATE INDEX idx_comment_saved_person ON comment_saved (person_id); + +CREATE INDEX idx_community_block_community ON community_block (community_id); + +CREATE INDEX idx_community_moderator_community ON community_moderator (community_id); + +CREATE INDEX idx_community_moderator_published ON community_moderator (published); + +CREATE INDEX idx_person_block_person ON person_block (person_id); + +CREATE INDEX idx_person_block_target ON person_block (target_id); + +CREATE INDEX idx_person_post_aggregates_person ON person_post_aggregates (person_id); + +CREATE INDEX idx_person_post_aggregates_post ON person_post_aggregates (post_id); + +CREATE INDEX idx_post_like_post ON post_like (post_id); + +DROP INDEX idx_person_actions_person, idx_person_actions_target, idx_post_actions_person, idx_post_actions_post; + +-- Drop `NOT NULL` indexes of columns that still exist +DROP INDEX idx_comment_actions_liked_not_null, idx_community_actions_followed_not_null, idx_person_actions_followed_not_null, idx_post_actions_read_not_null, idx_instance_actions_blocked_not_null; + +-- Drop statistics of columns that still exist +DROP statistics comment_actions_liked_stat, community_actions_followed_stat, person_actions_followed_stat; + diff --git a/migrations/2024-11-10-134311_smoosh-tables-together/up.sql b/migrations/2024-11-10-134311_smoosh-tables-together/up.sql new file mode 100644 index 000000000..aadf95692 --- /dev/null +++ b/migrations/2024-11-10-134311_smoosh-tables-together/up.sql @@ -0,0 +1,338 @@ +-- For each new actions table, transform the table previously used for the most common action type +-- into the new actions table, which should only change the table's metadata instead of rewriting the +-- rows +ALTER TABLE comment_like RENAME TO comment_actions; + +ALTER TABLE community_follower RENAME TO community_actions; + +ALTER TABLE instance_block RENAME TO instance_actions; + +ALTER TABLE person_follower RENAME TO person_actions; + +ALTER TABLE post_read RENAME TO post_actions; + +ALTER TABLE comment_actions RENAME COLUMN published TO liked; + +ALTER TABLE comment_actions RENAME COLUMN score TO like_score; + +ALTER TABLE community_actions RENAME COLUMN published TO followed; + +ALTER TABLE community_actions RENAME COLUMN state TO follow_state; + +ALTER TABLE community_actions RENAME COLUMN approver_id TO follow_approver_id; + +ALTER TABLE instance_actions RENAME COLUMN published TO blocked; + +ALTER TABLE person_actions RENAME COLUMN person_id TO target_id; + +ALTER TABLE person_actions RENAME COLUMN follower_id TO person_id; + +ALTER TABLE person_actions RENAME COLUMN published TO followed; + +ALTER TABLE person_actions RENAME COLUMN pending TO follow_pending; + +ALTER TABLE post_actions RENAME COLUMN published TO read; + +ALTER TABLE comment_actions + ALTER COLUMN liked DROP NOT NULL, + ALTER COLUMN liked DROP DEFAULT, + ALTER COLUMN like_score DROP NOT NULL, + ADD COLUMN saved timestamptz, + ADD CONSTRAINT comment_actions_check_liked CHECK ((liked IS NULL) = (like_score IS NULL)); + +ALTER TABLE community_actions + ALTER COLUMN followed DROP NOT NULL, + ALTER COLUMN followed DROP DEFAULT, + ALTER COLUMN follow_state DROP NOT NULL, + ADD COLUMN blocked timestamptz, + ADD COLUMN became_moderator timestamptz, + ADD COLUMN received_ban timestamptz, + ADD COLUMN ban_expires timestamptz, + ADD CONSTRAINT community_actions_check_followed CHECK ((followed IS NULL) = (follow_state IS NULL) AND NOT (followed IS NULL AND follow_approver_id IS NOT NULL)), + ADD CONSTRAINT community_actions_check_received_ban CHECK (NOT (received_ban IS NULL AND ban_expires IS NOT NULL)); + +ALTER TABLE instance_actions + ALTER COLUMN blocked DROP NOT NULL, + ALTER COLUMN blocked DROP DEFAULT; + +ALTER TABLE person_actions + ALTER COLUMN followed DROP NOT NULL, + ALTER COLUMN followed DROP DEFAULT, + ALTER COLUMN follow_pending DROP NOT NULL, + ADD COLUMN blocked timestamptz, + ADD CONSTRAINT person_actions_check_followed CHECK ((followed IS NULL) = (follow_pending IS NULL)); + +ALTER TABLE post_actions + ALTER COLUMN read DROP NOT NULL, + ALTER COLUMN read DROP DEFAULT, + ADD COLUMN read_comments timestamptz, + ADD COLUMN read_comments_amount bigint, + ADD COLUMN saved timestamptz, + ADD COLUMN liked timestamptz, + ADD COLUMN like_score smallint, + ADD COLUMN hidden timestamptz, + ADD CONSTRAINT post_actions_check_read_comments CHECK ((read_comments IS NULL) = (read_comments_amount IS NULL)), + ADD CONSTRAINT post_actions_check_liked CHECK ((liked IS NULL) = (like_score IS NULL)); + +-- Add actions from other old tables to the new tables +INSERT INTO comment_actions (person_id, comment_id, saved) +SELECT + person_id, + comment_id, + published +FROM + comment_saved +ON CONFLICT (person_id, + comment_id) + DO UPDATE SET + saved = excluded.saved; + +INSERT INTO community_actions (person_id, community_id, blocked) +SELECT + person_id, + community_id, + published +FROM + community_block +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + blocked = excluded.blocked; + +INSERT INTO community_actions (person_id, community_id, became_moderator) +SELECT + person_id, + community_id, + published +FROM + community_moderator +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + became_moderator = excluded.became_moderator; + +INSERT INTO community_actions (person_id, community_id, received_ban, ban_expires) +SELECT + person_id, + community_id, + published, + expires +FROM + community_person_ban +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + received_ban = excluded.received_ban, + ban_expires = excluded.ban_expires; + +INSERT INTO person_actions (person_id, target_id, blocked) +SELECT + person_id, + target_id, + published +FROM + person_block +ON CONFLICT (person_id, + target_id) + DO UPDATE SET + person_id = excluded.person_id, + target_id = excluded.target_id, + blocked = excluded.blocked; + +INSERT INTO post_actions (person_id, post_id, read_comments, read_comments_amount) +SELECT + person_id, + post_id, + published, + read_comments +FROM + person_post_aggregates +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + read_comments = excluded.read_comments, + read_comments_amount = excluded.read_comments_amount; + +INSERT INTO post_actions (person_id, post_id, hidden) +SELECT + person_id, + post_id, + published +FROM + post_hide +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + hidden = excluded.hidden; + +INSERT INTO post_actions (person_id, post_id, liked, like_score) +SELECT + person_id, + post_id, + published, + score +FROM + post_like +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + liked = excluded.liked, + like_score = excluded.like_score; + +INSERT INTO post_actions (person_id, post_id, saved) +SELECT + person_id, + post_id, + published +FROM + post_saved +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + saved = excluded.saved; + +-- Drop old tables +DROP TABLE comment_saved, community_block, community_moderator, community_person_ban, person_block, person_post_aggregates, post_hide, post_like, post_saved; + +-- Rename associated stuff +ALTER INDEX comment_like_pkey RENAME TO comment_actions_pkey; + +ALTER INDEX idx_comment_like_comment RENAME TO idx_comment_actions_comment; + +ALTER TABLE comment_actions RENAME CONSTRAINT comment_like_comment_id_fkey TO comment_actions_comment_id_fkey; + +ALTER TABLE comment_actions RENAME CONSTRAINT comment_like_person_id_fkey TO comment_actions_person_id_fkey; + +ALTER INDEX community_follower_pkey RENAME TO community_actions_pkey; + +ALTER INDEX idx_community_follower_community RENAME TO idx_community_actions_community; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_community_id_fkey TO community_actions_community_id_fkey; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_person_id_fkey TO community_actions_person_id_fkey; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_approver_id_fkey TO community_actions_follow_approver_id_fkey; + +ALTER INDEX instance_block_pkey RENAME TO instance_actions_pkey; + +ALTER TABLE instance_actions RENAME CONSTRAINT instance_block_instance_id_fkey TO instance_actions_instance_id_fkey; + +ALTER TABLE instance_actions RENAME CONSTRAINT instance_block_person_id_fkey TO instance_actions_person_id_fkey; + +ALTER INDEX person_follower_pkey RENAME TO person_actions_pkey; + +ALTER TABLE person_actions RENAME CONSTRAINT person_follower_person_id_fkey TO person_actions_target_id_fkey; + +ALTER TABLE person_actions RENAME CONSTRAINT person_follower_follower_id_fkey TO person_actions_person_id_fkey; + +ALTER INDEX post_read_pkey RENAME TO post_actions_pkey; + +ALTER TABLE post_actions RENAME CONSTRAINT post_read_person_id_fkey TO post_actions_person_id_fkey; + +ALTER TABLE post_actions RENAME CONSTRAINT post_read_post_id_fkey TO post_actions_post_id_fkey; + +-- Rename idx_community_follower_published and add filter +CREATE INDEX idx_community_actions_followed ON community_actions (followed) +WHERE + followed IS NOT NULL; + +DROP INDEX idx_community_follower_published; + +-- Restore indexes of dropped tables +CREATE INDEX idx_community_actions_became_moderator ON community_actions (became_moderator) +WHERE + became_moderator IS NOT NULL; + +CREATE INDEX idx_person_actions_person ON person_actions (person_id); + +CREATE INDEX idx_person_actions_target ON person_actions (target_id); + +CREATE INDEX idx_post_actions_person ON post_actions (person_id); + +CREATE INDEX idx_post_actions_post ON post_actions (post_id); + +-- Create new indexes, with `OR` being used to allow `IS NOT NULL` filters in queries to use either column in +-- a group (e.g. `liked IS NOT NULL` and `like_score IS NOT NULL` both work) +CREATE INDEX idx_comment_actions_liked_not_null ON comment_actions (person_id, comment_id) +WHERE + liked IS NOT NULL OR like_score IS NOT NULL; + +CREATE INDEX idx_comment_actions_saved_not_null ON comment_actions (person_id, comment_id) +WHERE + saved IS NOT NULL; + +CREATE INDEX idx_community_actions_followed_not_null ON community_actions (person_id, community_id) +WHERE + followed IS NOT NULL OR follow_state IS NOT NULL; + +CREATE INDEX idx_community_actions_blocked_not_null ON community_actions (person_id, community_id) +WHERE + blocked IS NOT NULL; + +CREATE INDEX idx_community_actions_became_moderator_not_null ON community_actions (person_id, community_id) +WHERE + became_moderator IS NOT NULL; + +CREATE INDEX idx_community_actions_received_ban_not_null ON community_actions (person_id, community_id) +WHERE + received_ban IS NOT NULL; + +CREATE INDEX idx_person_actions_followed_not_null ON person_actions (person_id, target_id) +WHERE + followed IS NOT NULL OR follow_pending IS NOT NULL; + +CREATE INDEX idx_person_actions_blocked_not_null ON person_actions (person_id, target_id) +WHERE + blocked IS NOT NULL; + +CREATE INDEX idx_post_actions_read_not_null ON post_actions (person_id, post_id) +WHERE + read IS NOT NULL; + +CREATE INDEX idx_post_actions_read_comments_not_null ON post_actions (person_id, post_id) +WHERE + read_comments IS NOT NULL OR read_comments_amount IS NOT NULL; + +CREATE INDEX idx_post_actions_saved_not_null ON post_actions (person_id, post_id) +WHERE + saved IS NOT NULL; + +CREATE INDEX idx_post_actions_liked_not_null ON post_actions (person_id, post_id) +WHERE + liked IS NOT NULL OR like_score IS NOT NULL; + +CREATE INDEX idx_post_actions_hidden_not_null ON post_actions (person_id, post_id) +WHERE + hidden IS NOT NULL; + +-- This index is currently redundant because instance_actions only has 1 action type, but inconsistency +-- with other tables would make it harder to do everything correctly when adding another action type +CREATE INDEX idx_instance_actions_blocked_not_null ON instance_actions (person_id, instance_id) +WHERE + blocked IS NOT NULL; + +-- Create new statistics for more accurate estimations of how much of an index will be read (e.g. for +-- `(liked, like_score)`, the query planner might othewise assume that `(TRUE, FALSE)` and `(TRUE, TRUE)` +-- are equally likely when only `(TRUE, TRUE)` is possible, which would make it severely underestimate +-- the efficiency of using the index) +CREATE statistics comment_actions_liked_stat ON (liked IS NULL), (like_score IS NULL) +FROM comment_actions; + +CREATE statistics community_actions_followed_stat ON (followed IS NULL), (follow_state IS NULL) +FROM community_actions; + +CREATE statistics person_actions_followed_stat ON (followed IS NULL), (follow_pending IS NULL) +FROM person_actions; + +CREATE statistics post_actions_read_comments_stat ON (read_comments IS NULL), (read_comments_amount IS NULL) +FROM post_actions; + +CREATE statistics post_actions_liked_stat ON (liked IS NULL), (like_score IS NULL), (post_id IS NULL) +FROM post_actions; + diff --git a/migrations/2024-11-12-090437_move-triggers/down.sql b/migrations/2024-11-12-090437_move-triggers/down.sql new file mode 100644 index 000000000..3607679bc --- /dev/null +++ b/migrations/2024-11-12-090437_move-triggers/down.sql @@ -0,0 +1,115 @@ +-- Edit community aggregates to include voters as active users +CREATE OR REPLACE FUNCTION community_aggregates_activity (i text) + RETURNS TABLE ( + count_ bigint, + community_id_ integer) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN query + SELECT + count(*), + community_id + FROM ( + SELECT + c.creator_id, + p.community_id + FROM + comment c + INNER JOIN post p ON c.post_id = p.id + INNER JOIN person pe ON c.creator_id = pe.id + WHERE + c.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + p.creator_id, + p.community_id + FROM + post p + INNER JOIN person pe ON p.creator_id = pe.id + WHERE + p.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + pl.person_id, + p.community_id + FROM + post_like pl + INNER JOIN post p ON pl.post_id = p.id + INNER JOIN person pe ON pl.person_id = pe.id + WHERE + pl.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + cl.person_id, + p.community_id + FROM + comment_like cl + INNER JOIN comment c ON cl.comment_id = comment.id + INNER JOIN post p ON comment.post_id = p.id + INNER JOIN person pe ON cl.person_id = pe.id + WHERE + cl.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE) a +GROUP BY + community_id; +END; +$$; + +-- Edit site aggregates to include voters and people who have read posts as active users +CREATE OR REPLACE FUNCTION site_aggregates_activity (i text) + RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE + count_ integer; +BEGIN + SELECT + count(*) INTO count_ + FROM ( + SELECT + c.creator_id + FROM + comment c + INNER JOIN person pe ON c.creator_id = pe.id + WHERE + c.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + p.creator_id + FROM + post p + INNER JOIN person pe ON p.creator_id = pe.id + WHERE + p.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + pl.person_id + FROM + post_like pl + INNER JOIN person pe ON pl.person_id = pe.id + WHERE + pl.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + cl.person_id + FROM + comment_like cl + INNER JOIN person pe ON cl.person_id = pe.id + WHERE + cl.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE) a; + RETURN count_; +END; +$$; + diff --git a/migrations/2024-11-12-090437_move-triggers/up.sql b/migrations/2024-11-12-090437_move-triggers/up.sql new file mode 100644 index 000000000..e7b2bd49d --- /dev/null +++ b/migrations/2024-11-12-090437_move-triggers/up.sql @@ -0,0 +1,2 @@ +DROP FUNCTION community_aggregates_activity, site_aggregates_activity CASCADE; + diff --git a/migrations/2024-11-21-195004_add_report_count/down.sql b/migrations/2024-11-21-195004_add_report_count/down.sql new file mode 100644 index 000000000..be418840f --- /dev/null +++ b/migrations/2024-11-21-195004_add_report_count/down.sql @@ -0,0 +1,8 @@ +ALTER TABLE post_aggregates + DROP COLUMN report_count, + DROP COLUMN unresolved_report_count; + +ALTER TABLE comment_aggregates + DROP COLUMN report_count, + DROP COLUMN unresolved_report_count; + diff --git a/migrations/2024-11-21-195004_add_report_count/up.sql b/migrations/2024-11-21-195004_add_report_count/up.sql new file mode 100644 index 000000000..c7d28e1ef --- /dev/null +++ b/migrations/2024-11-21-195004_add_report_count/up.sql @@ -0,0 +1,79 @@ +-- Adding report_count and unresolved_report_count +-- to the post and comment aggregate tables +ALTER TABLE post_aggregates + ADD COLUMN report_count smallint NOT NULL DEFAULT 0, + ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0; + +ALTER TABLE comment_aggregates + ADD COLUMN report_count smallint NOT NULL DEFAULT 0, + ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0; + +-- Update the historical counts +-- Posts +UPDATE + post_aggregates AS a +SET + report_count = cnt.count +FROM ( + SELECT + post_id, + count(*) AS count + FROM + post_report + GROUP BY + post_id) cnt +WHERE + a.post_id = cnt.post_id; + +-- The unresolved +UPDATE + post_aggregates AS a +SET + unresolved_report_count = cnt.count +FROM ( + SELECT + post_id, + count(*) AS count + FROM + post_report + WHERE + resolved = 'f' + GROUP BY + post_id) cnt +WHERE + a.post_id = cnt.post_id; + +-- Comments +UPDATE + comment_aggregates AS a +SET + report_count = cnt.count +FROM ( + SELECT + comment_id, + count(*) AS count + FROM + comment_report + GROUP BY + comment_id) cnt +WHERE + a.comment_id = cnt.comment_id; + +-- The unresolved +UPDATE + comment_aggregates AS a +SET + unresolved_report_count = cnt.count +FROM ( + SELECT + comment_id, + count(*) AS count + FROM + comment_report + WHERE + resolved = 'f' + GROUP BY + comment_id) cnt +WHERE + a.comment_id = cnt.comment_id; + diff --git a/migrations/2024-11-28-142005_instance-block-mod-log/down.sql b/migrations/2024-11-28-142005_instance-block-mod-log/down.sql new file mode 100644 index 000000000..7936cfe7c --- /dev/null +++ b/migrations/2024-11-28-142005_instance-block-mod-log/down.sql @@ -0,0 +1,7 @@ +ALTER TABLE federation_blocklist + DROP expires; + +DROP TABLE admin_block_instance; + +DROP TABLE admin_allow_instance; + diff --git a/migrations/2024-11-28-142005_instance-block-mod-log/up.sql b/migrations/2024-11-28-142005_instance-block-mod-log/up.sql new file mode 100644 index 000000000..f537f5d32 --- /dev/null +++ b/migrations/2024-11-28-142005_instance-block-mod-log/up.sql @@ -0,0 +1,22 @@ +ALTER TABLE federation_blocklist + ADD COLUMN expires timestamptz; + +CREATE TABLE admin_block_instance ( + id serial PRIMARY KEY, + instance_id int NOT NULL REFERENCES instance (id) ON UPDATE CASCADE ON DELETE CASCADE, + admin_person_id int NOT NULL REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE, + blocked bool NOT NULL, + reason text, + expires timestamptz, + when_ timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE admin_allow_instance ( + id serial PRIMARY KEY, + instance_id int NOT NULL REFERENCES instance (id) ON UPDATE CASCADE ON DELETE CASCADE, + admin_person_id int NOT NULL REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE, + allowed bool NOT NULL, + reason text, + when_ timestamptz NOT NULL DEFAULT now() +); + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 8f06e0610..4a648da00 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -1,4 +1,4 @@ -use actix_web::{guard, web}; +use actix_web::{guard, web::*}; use lemmy_api::{ comment::{ distinguish::distinguish_comment, @@ -60,6 +60,7 @@ use lemmy_api::{ like::like_post, list_post_likes::list_post_likes, lock::lock_post, + mark_many_read::mark_posts_as_read, mark_read::mark_post_as_read, save::save_post, }, @@ -75,7 +76,8 @@ use lemmy_api::{ resolve::resolve_pm_report, }, site::{ - block::block_instance, + admin_allow_instance::admin_allow_instance, + admin_block_instance::admin_block_instance, federated_instances::get_federated_instances, leave_admin::leave_admin, list_all_media::list_all_media, @@ -92,6 +94,7 @@ use lemmy_api::{ list::list_registration_applications, unread_count::get_unread_registration_application_count, }, + user_block_instance::user_block_instance, }, sitemap::get_sitemap, }; @@ -158,148 +161,146 @@ use lemmy_apub::api::{ use lemmy_routes::images::image_proxy; use lemmy_utils::rate_limit::RateLimitCell; -pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { +pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { cfg.service( - web::scope("/api/v3") - .route("/image_proxy", web::get().to(image_proxy)) + scope("/api/v3") + .route("/image_proxy", get().to(image_proxy)) // Site .service( - web::scope("/site") + scope("/site") .wrap(rate_limit.message()) - .route("", web::get().to(get_site)) + .route("", get().to(get_site)) // Admin Actions - .route("", web::post().to(create_site)) - .route("", web::put().to(update_site)) - .route("/block", web::post().to(block_instance)), + .route("", post().to(create_site)) + .route("", put().to(update_site)) + .route("/block", post().to(user_block_instance)), ) .service( - web::resource("/modlog") + resource("/modlog") .wrap(rate_limit.message()) - .route(web::get().to(get_mod_log)), + .route(get().to(get_mod_log)), ) .service( - web::resource("/search") + resource("/search") .wrap(rate_limit.search()) - .route(web::get().to(search)), + .route(get().to(search)), ) .service( - web::resource("/resolve_object") + resource("/resolve_object") .wrap(rate_limit.message()) - .route(web::get().to(resolve_object)), + .route(get().to(resolve_object)), ) // Community .service( - web::resource("/community") + resource("/community") .guard(guard::Post()) .wrap(rate_limit.register()) - .route(web::post().to(create_community)), + .route(post().to(create_community)), ) .service( - web::scope("/community") + scope("/community") .wrap(rate_limit.message()) - .route("", web::get().to(get_community)) - .route("", web::put().to(update_community)) - .route("/random", web::get().to(get_random_community)) - .route("/hide", web::put().to(hide_community)) - .route("/list", web::get().to(list_communities)) - .route("/follow", web::post().to(follow_community)) - .route("/block", web::post().to(block_community)) - .route("/delete", web::post().to(delete_community)) - // .route("/post_tags", web::get().to(get_community_post_tags)) + .route("", get().to(get_community)) + .route("", put().to(update_community)) + .route("/random", get().to(get_random_community)) + .route("/hide", put().to(hide_community)) + .route("/list", get().to(list_communities)) + .route("/follow", post().to(follow_community)) + .route("/block", post().to(block_community)) + .route("/delete", post().to(delete_community)) // Mod Actions - // .route("/post_tags", web::post().to(create_update_community_post_tag)) - // .route("/post_tags/delete", web::post().to(delete_community_post_tag)), - .route("/remove", web::post().to(remove_community)) - .route("/transfer", web::post().to(transfer_community)) - .route("/ban_user", web::post().to(ban_from_community)) - .route("/mod", web::post().to(add_mod_to_community)) + .route("/remove", post().to(remove_community)) + .route("/transfer", post().to(transfer_community)) + .route("/ban_user", post().to(ban_from_community)) + .route("/mod", post().to(add_mod_to_community)) .service( - web::scope("/pending_follows") + scope("/pending_follows") .wrap(rate_limit.message()) - .route("/count", web::get().to(get_pending_follows_count)) - .route("/list", web::get().to(get_pending_follows_list)) - .route("/approve", web::post().to(post_pending_follows_approve)), + .route("/count", get().to(get_pending_follows_count)) + .route("/list", get().to(get_pending_follows_list)) + .route("/approve", post().to(post_pending_follows_approve)), ), ) .service( - web::scope("/federated_instances") + scope("/federated_instances") .wrap(rate_limit.message()) - .route("", web::get().to(get_federated_instances)), + .route("", get().to(get_federated_instances)), ) // Post .service( // Handle POST to /post separately to add the post() rate limitter - web::resource("/post") + resource("/post") .guard(guard::Post()) .wrap(rate_limit.post()) - .route(web::post().to(create_post)), + .route(post().to(create_post)), ) .service( - web::scope("/post") + scope("/post") .wrap(rate_limit.message()) - .route("", web::get().to(get_post)) - .route("", web::put().to(update_post)) - .route("/delete", web::post().to(delete_post)) - .route("/remove", web::post().to(remove_post)) - .route("/mark_as_read", web::post().to(mark_post_as_read)) - .route("/hide", web::post().to(hide_post)) - .route("/lock", web::post().to(lock_post)) - .route("/feature", web::post().to(feature_post)) - .route("/list", web::get().to(list_posts)) - .route("/like", web::post().to(like_post)) - .route("/like/list", web::get().to(list_post_likes)) - .route("/save", web::put().to(save_post)) - .route("/report", web::post().to(create_post_report)) - .route("/report/resolve", web::put().to(resolve_post_report)) - .route("/report/list", web::get().to(list_post_reports)) - .route("/site_metadata", web::get().to(get_link_metadata)), + .route("", get().to(get_post)) + .route("", put().to(update_post)) + .route("/delete", post().to(delete_post)) + .route("/remove", post().to(remove_post)) + .route("/mark_as_read", post().to(mark_post_as_read)) + .route("/mark_many_as_read", post().to(mark_posts_as_read)) + .route("/hide", post().to(hide_post)) + .route("/lock", post().to(lock_post)) + .route("/feature", post().to(feature_post)) + .route("/list", get().to(list_posts)) + .route("/like", post().to(like_post)) + .route("/like/list", get().to(list_post_likes)) + .route("/save", put().to(save_post)) + .route("/report", post().to(create_post_report)) + .route("/report/resolve", put().to(resolve_post_report)) + .route("/report/list", get().to(list_post_reports)) + .route("/site_metadata", get().to(get_link_metadata)), ) // Comment .service( // Handle POST to /comment separately to add the comment() rate limitter - web::resource("/comment") + resource("/comment") .guard(guard::Post()) .wrap(rate_limit.comment()) - .route(web::post().to(create_comment)), + .route(post().to(create_comment)), ) .service( - web::scope("/comment") + scope("/comment") .wrap(rate_limit.message()) - .route("", web::get().to(get_comment)) - .route("", web::put().to(update_comment)) - .route("/delete", web::post().to(delete_comment)) - .route("/remove", web::post().to(remove_comment)) - .route("/mark_as_read", web::post().to(mark_reply_as_read)) - .route("/distinguish", web::post().to(distinguish_comment)) - .route("/like", web::post().to(like_comment)) - .route("/like/list", web::get().to(list_comment_likes)) - .route("/save", web::put().to(save_comment)) - .route("/list", web::get().to(list_comments)) - .route("/report", web::post().to(create_comment_report)) - .route("/report/resolve", web::put().to(resolve_comment_report)) - .route("/report/list", web::get().to(list_comment_reports)), + .route("", get().to(get_comment)) + .route("", put().to(update_comment)) + .route("/delete", post().to(delete_comment)) + .route("/remove", post().to(remove_comment)) + .route("/mark_as_read", post().to(mark_reply_as_read)) + .route("/distinguish", post().to(distinguish_comment)) + .route("/like", post().to(like_comment)) + .route("/like/list", get().to(list_comment_likes)) + .route("/save", put().to(save_comment)) + .route("/list", get().to(list_comments)) + .route("/report", post().to(create_comment_report)) + .route("/report/resolve", put().to(resolve_comment_report)) + .route("/report/list", get().to(list_comment_reports)), ) // Private Message .service( - web::scope("/private_message") + scope("/private_message") .wrap(rate_limit.message()) - .route("/list", web::get().to(get_private_message)) - .route("", web::post().to(create_private_message)) - .route("", web::put().to(update_private_message)) - .route("/delete", web::post().to(delete_private_message)) - .route("/mark_as_read", web::post().to(mark_pm_as_read)) - .route("/report", web::post().to(create_pm_report)) - .route("/report/resolve", web::put().to(resolve_pm_report)) - .route("/report/list", web::get().to(list_pm_reports)), + .route("/list", get().to(get_private_message)) + .route("", post().to(create_private_message)) + .route("", put().to(update_private_message)) + .route("/delete", post().to(delete_private_message)) + .route("/mark_as_read", post().to(mark_pm_as_read)) + .route("/report", post().to(create_pm_report)) + .route("/report/resolve", put().to(resolve_pm_report)) + .route("/report/list", get().to(list_pm_reports)), ) // User .service( // Account action, I don't like that it's in /user maybe /accounts // Handle /user/register separately to add the register() rate limiter - web::resource("/user/register") + resource("/user/register") .guard(guard::Post()) .wrap(rate_limit.register()) - .route(web::post().to(register)), + .route(post().to(register)), ) // User .service( @@ -307,138 +308,134 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { // 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") + resource("/user/login") .guard(guard::Post()) .wrap(rate_limit.register()) - .route(web::post().to(login)), + .route(post().to(login)), ) .service( - web::resource("/user/password_reset") + resource("/user/password_reset") .wrap(rate_limit.register()) - .route(web::post().to(reset_password)), + .route(post().to(reset_password)), ) .service( // Handle captcha separately - web::resource("/user/get_captcha") + resource("/user/get_captcha") .wrap(rate_limit.post()) - .route(web::get().to(get_captcha)), + .route(get().to(get_captcha)), ) .service( - web::resource("/user/export_settings") + resource("/user/export_settings") .wrap(rate_limit.import_user_settings()) - .route(web::get().to(export_settings)), + .route(get().to(export_settings)), ) .service( - web::resource("/user/import_settings") + resource("/user/import_settings") .wrap(rate_limit.import_user_settings()) - .route(web::post().to(import_settings)), + .route(post().to(import_settings)), ) // TODO, all the current account related actions under /user need to get moved here eventually .service( - web::scope("/account") + scope("/account") .wrap(rate_limit.message()) - .route("/list_media", web::get().to(list_media)), + .route("/list_media", get().to(list_media)), ) // User actions .service( - web::scope("/user") + scope("/user") .wrap(rate_limit.message()) - .route("", web::get().to(read_person)) - .route("/mention", web::get().to(list_mentions)) + .route("", get().to(read_person)) + .route("/mention", get().to(list_mentions)) .route( "/mention/mark_as_read", - web::post().to(mark_person_mention_as_read), + post().to(mark_person_mention_as_read), ) - .route("/replies", web::get().to(list_replies)) + .route("/replies", get().to(list_replies)) // Admin action. I don't like that it's in /user - .route("/ban", web::post().to(ban_from_site)) - .route("/banned", web::get().to(list_banned_users)) - .route("/block", web::post().to(block_person)) + .route("/ban", post().to(ban_from_site)) + .route("/banned", get().to(list_banned_users)) + .route("/block", post().to(block_person)) // TODO Account actions. I don't like that they're in /user maybe /accounts - .route("/logout", web::post().to(logout)) - .route("/delete_account", web::post().to(delete_account)) - .route( - "/password_change", - web::post().to(change_password_after_reset), - ) + .route("/logout", post().to(logout)) + .route("/delete_account", post().to(delete_account)) + .route("/password_change", post().to(change_password_after_reset)) // TODO mark_all_as_read feels off being in this section as well - .route( - "/mark_all_as_read", - web::post().to(mark_all_notifications_read), - ) - .route("/save_user_settings", web::put().to(save_user_settings)) - .route("/change_password", web::put().to(change_password)) - .route("/report_count", web::get().to(report_count)) - .route("/unread_count", web::get().to(unread_count)) - .route("/verify_email", web::post().to(verify_email)) - .route("/leave_admin", web::post().to(leave_admin)) - .route("/totp/generate", web::post().to(generate_totp_secret)) - .route("/totp/update", web::post().to(update_totp)) - .route("/list_logins", web::get().to(list_logins)) - .route("/validate_auth", web::get().to(validate_auth)), + .route("/mark_all_as_read", post().to(mark_all_notifications_read)) + .route("/save_user_settings", put().to(save_user_settings)) + .route("/change_password", put().to(change_password)) + .route("/report_count", get().to(report_count)) + .route("/unread_count", get().to(unread_count)) + .route("/verify_email", post().to(verify_email)) + .route("/leave_admin", post().to(leave_admin)) + .route("/totp/generate", post().to(generate_totp_secret)) + .route("/totp/update", post().to(update_totp)) + .route("/list_logins", get().to(list_logins)) + .route("/validate_auth", get().to(validate_auth)), ) // Admin Actions .service( - web::scope("/admin") + scope("/admin") .wrap(rate_limit.message()) - .route("/add", web::post().to(add_admin)) + .route("/add", post().to(add_admin)) .route( "/registration_application/count", - web::get().to(get_unread_registration_application_count), + get().to(get_unread_registration_application_count), ) .route( "/registration_application/list", - web::get().to(list_registration_applications), + get().to(list_registration_applications), ) .route( "/registration_application/approve", - web::put().to(approve_registration_application), + put().to(approve_registration_application), ) .route( "/registration_application", - web::get().to(get_registration_application), + get().to(get_registration_application), ) - .route("/list_all_media", web::get().to(list_all_media)) + .route("/list_all_media", get().to(list_all_media)) .service( - web::scope("/purge") - .route("/person", web::post().to(purge_person)) - .route("/community", web::post().to(purge_community)) - .route("/post", web::post().to(purge_post)) - .route("/comment", web::post().to(purge_comment)), + scope("/purge") + .route("/person", post().to(purge_person)) + .route("/community", post().to(purge_community)) + .route("/post", post().to(purge_post)) + .route("/comment", post().to(purge_comment)), ) .service( - web::scope("/tagline") + scope("/tagline") .wrap(rate_limit.message()) - .route("", web::post().to(create_tagline)) - .route("", web::put().to(update_tagline)) - .route("/delete", web::post().to(delete_tagline)) - .route("/list", web::get().to(list_taglines)), - ), + .route("", post().to(create_tagline)) + .route("", put().to(update_tagline)) + .route("/delete", post().to(delete_tagline)) + .route("/list", get().to(list_taglines)), + ) + .route("block_instance", post().to(admin_block_instance)) + .route("allow_instance", post().to(admin_allow_instance)), ) .service( - web::scope("/custom_emoji") + scope("/custom_emoji") .wrap(rate_limit.message()) - .route("", web::post().to(create_custom_emoji)) - .route("", web::put().to(update_custom_emoji)) - .route("/delete", web::post().to(delete_custom_emoji)) - .route("/list", web::get().to(list_custom_emojis)), + .route("", post().to(create_custom_emoji)) + .route("", put().to(update_custom_emoji)) + .route("/delete", post().to(delete_custom_emoji)) + .route("/list", get().to(list_custom_emojis)), ) .service( - web::scope("/oauth_provider") + scope("/oauth_provider") .wrap(rate_limit.message()) - .route("", web::post().to(create_oauth_provider)) - .route("", web::put().to(update_oauth_provider)) - .route("/delete", web::post().to(delete_oauth_provider)), + .route("", post().to(create_oauth_provider)) + .route("", put().to(update_oauth_provider)) + .route("/delete", post().to(delete_oauth_provider)), ) .service( - web::scope("/oauth") + scope("/oauth") .wrap(rate_limit.register()) - .route("/authenticate", web::post().to(authenticate_with_oauth)), + .route("/authenticate", post().to(authenticate_with_oauth)), ), ); cfg.service( - web::scope("/sitemap.xml") + scope("/sitemap.xml") .wrap(rate_limit.message()) - .route("", web::get().to(get_sitemap)), + .route("", get().to(get_sitemap)), ); } diff --git a/src/code_migrations.rs b/src/code_migrations.rs index 84af43ea7..1c23a3f00 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -1,5 +1,6 @@ // This is for db migrations that require code use activitypub_federation::http_signatures::generate_actor_keypair; +use chrono::Utc; use diesel::{ sql_types::{Nullable, Text}, ExpressionMethods, @@ -26,9 +27,12 @@ use lemmy_db_schema::{ site::{Site, SiteInsertForm, SiteUpdateForm}, }, traits::Crud, - utils::{get_conn, naive_now, DbPool}, + utils::{get_conn, DbPool}, +}; +use lemmy_utils::{ + error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, + settings::structs::Settings, }; -use lemmy_utils::{error::LemmyResult, settings::structs::Settings}; use tracing::info; use url::Url; @@ -78,7 +82,7 @@ async fn user_updates_2020_04_02( )?), private_key: Some(Some(keypair.private_key)), public_key: Some(keypair.public_key), - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), ..Default::default() }; @@ -118,7 +122,7 @@ async fn community_updates_2020_04_02( actor_id: Some(community_actor_id.clone()), private_key: Some(Some(keypair.private_key)), public_key: Some(keypair.public_key), - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), ..Default::default() }; @@ -334,7 +338,7 @@ async fn instance_actor_2022_01_28( let actor_id = Url::parse(protocol_and_hostname)?; let site_form = SiteUpdateForm { actor_id: Some(actor_id.clone().into()), - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), inbox_url: Some(generate_inbox_url()?), private_key: Some(Some(key_pair.private_key)), public_key: Some(key_pair.public_key), @@ -420,7 +424,7 @@ async fn initialize_local_site_2022_10_10( let domain = settings .get_hostname_without_port() - .expect("must have domain"); + .with_lemmy_type(LemmyErrorType::Unknown("must have domain".into()))?; // Upsert this to the instance table let instance = Instance::read_or_create(pool, domain).await?; @@ -465,7 +469,7 @@ async fn initialize_local_site_2022_10_10( .unwrap_or_else(|| "New Site".to_string()); let site_form = SiteInsertForm { actor_id: Some(site_actor_id.clone().into()), - last_refreshed_at: Some(naive_now()), + last_refreshed_at: Some(Utc::now()), inbox_url: Some(generate_inbox_url()?), private_key: Some(site_key_pair.private_key), public_key: Some(site_key_pair.public_key), diff --git a/src/lib.rs b/src/lib.rs index 9da09f65b..319efd224 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ use lemmy_db_schema::{source::secret::Secret, utils::build_db_pool}; use lemmy_federate::{Opts, SendManager}; use lemmy_routes::{feeds, images, nodeinfo, webfinger}; use lemmy_utils::{ - error::LemmyResult, + error::{LemmyErrorType, LemmyResult}, rate_limit::RateLimitCell, response::jsonify_plain_text_errors, settings::{structs::Settings, SETTINGS}, @@ -178,7 +178,8 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { .set(Box::new(move |d, c| { Box::pin(match_outgoing_activities(d, c)) })) - .expect("set function pointer"); + .map_err(|_e| LemmyErrorType::Unknown("couldnt set function pointer".into()))?; + let request_data = federation_config.to_request_data(); let outgoing_activities_task = tokio::task::spawn(handle_outgoing_activities( request_data.reset_request_count(), @@ -281,7 +282,7 @@ fn create_http_server( let prom_api_metrics = PrometheusMetricsBuilder::new("lemmy_api") .registry(default_registry().clone()) .build() - .expect("Should always be buildable"); + .map_err(|e| LemmyErrorType::Unknown(format!("Should always be buildable: {e}")))?; let context: LemmyContext = federation_config.deref().clone(); let rate_limit_cell = federation_config.rate_limit_cell().clone(); @@ -339,23 +340,31 @@ fn create_http_server( fn cors_config(settings: &Settings) -> Cors { let self_origin = settings.get_protocol_and_hostname(); let cors_origin_setting = settings.cors_origin(); + + // A default setting for either wildcard, or None + let cors_default = Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .expose_any_header() + .max_age(3600); + 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 - if cors_origin_setting.as_deref() == Some("*") { - Cors::default().allow_any_origin().send_wildcard() + if origin == "*" { + cors_default } else { Cors::default() .allowed_origin(&origin) .allowed_origin(&self_origin) + .allow_any_method() + .allow_any_header() + .expose_any_header() + .max_age(3600) } } - _ => Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header() - .expose_any_header() - .max_age(3600), + _ => cors_default, } } diff --git a/src/main.rs b/src/main.rs index 6babedff4..73cd0c1a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use clap::Parser; use lemmy_server::{start_lemmy_server, CmdArgs}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use tracing::level_filters::LevelFilter; use tracing_subscriber::EnvFilter; @@ -17,7 +17,7 @@ pub async fn main() -> LemmyResult<()> { rustls::crypto::ring::default_provider() .install_default() - .expect("Failed to install rustls crypto provider"); + .map_err(|_e| LemmyErrorType::Unknown("Failed to install rustls crypto provider".into()))?; start_lemmy_server(args).await?; Ok(()) diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 37f2ff809..52962877f 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -22,7 +22,8 @@ use lemmy_db_schema::{ captcha_answer, comment, community, - community_person_ban, + community_actions, + federation_blocklist, instance, person, post, @@ -36,7 +37,7 @@ use lemmy_db_schema::{ post::{Post, PostUpdateForm}, }, traits::Crud, - utils::{functions::coalesce, get_conn, naive_now, now, DbPool, DELETED_REPLACEMENT_TEXT}, + utils::{find_action, functions::coalesce, get_conn, now, DbPool, DELETED_REPLACEMENT_TEXT}, }; use lemmy_routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use lemmy_utils::error::LemmyResult; @@ -58,6 +59,7 @@ pub async fn setup(context: Data) -> LemmyResult<()> { async move { active_counts(&mut context.pool()).await; update_banned_when_expired(&mut context.pool()).await; + delete_instance_block_when_expired(&mut context.pool()).await; } }); @@ -113,6 +115,7 @@ async fn startup_jobs(pool: &mut DbPool<'_>) { active_counts(pool).await; update_hot_ranks(pool).await; update_banned_when_expired(pool).await; + delete_instance_block_when_expired(pool).await; clear_old_activities(pool).await; overwrite_deleted_posts_and_comments(pool).await; delete_old_denied_users(pool).await; @@ -169,10 +172,7 @@ async fn process_ranks_in_batches( where_clause: &str, set_clause: &str, ) { - let process_start_time: DateTime = Utc - .timestamp_opt(0, 0) - .single() - .expect("0 timestamp creation"); + let process_start_time: DateTime = Utc.timestamp_opt(0, 0).single().unwrap_or_default(); let update_batch_size = 1000; // Bigger batches than this tend to cause seq scans let mut processed_rows_count = 0; @@ -220,10 +220,7 @@ async fn process_ranks_in_batches( /// Post aggregates is a special case, since it needs to join to the community_aggregates /// table, to get the active monthly user counts. async fn process_post_aggregates_ranks_in_batches(conn: &mut AsyncPgConnection) { - let process_start_time: DateTime = Utc - .timestamp_opt(0, 0) - .single() - .expect("0 timestamp creation"); + let process_start_time: DateTime = Utc.timestamp_opt(0, 0).single().unwrap_or_default(); let update_batch_size = 1000; // Bigger batches than this tend to cause seq scans let mut processed_rows_count = 0; @@ -395,7 +392,7 @@ async fn active_counts(pool: &mut DbPool<'_>) { for (full_form, abbr) in &intervals { let update_site_stmt = format!( - "update site_aggregates set users_active_{} = (select * from site_aggregates_activity('{}')) where site_id = 1", + "update site_aggregates set users_active_{} = (select * from r.site_aggregates_activity('{}')) where site_id = 1", abbr, full_form ); sql_query(update_site_stmt) @@ -404,7 +401,7 @@ async fn active_counts(pool: &mut DbPool<'_>) { .inspect_err(|e| error!("Failed to update site stats: {e}")) .ok(); - let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", abbr, full_form); + let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from r.community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", abbr, full_form); sql_query(update_community_stmt) .execute(&mut conn) .await @@ -439,7 +436,7 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { .ok(); diesel::delete( - community_person_ban::table.filter(community_person_ban::expires.lt(now().nullable())), + community_actions::table.filter(community_actions::ban_expires.lt(now().nullable())), ) .execute(&mut conn) .await @@ -452,6 +449,27 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { } } +/// Set banned to false after ban expires +async fn delete_instance_block_when_expired(pool: &mut DbPool<'_>) { + info!("Delete instance blocks when expired ..."); + let conn = get_conn(pool).await; + + match conn { + Ok(mut conn) => { + diesel::delete( + federation_blocklist::table.filter(federation_blocklist::expires.lt(now().nullable())), + ) + .execute(&mut conn) + .await + .inspect_err(|e| error!("Failed to remove federation_blocklist expired rows: {e}")) + .ok(); + } + Err(e) => { + error!("Failed to get connection from pool: {e}"); + } + } +} + /// Find all unpublished posts with scheduled date in the future, and publish them. async fn publish_scheduled_posts(context: &Data) { let pool = &mut context.pool(); @@ -470,11 +488,10 @@ async fn publish_scheduled_posts(context: &Data) { .filter(not(person::banned.or(person::deleted))) .filter(not(community::removed.or(community::deleted))) // ensure that user isnt banned from community - .filter(not(exists( - community_person_ban::table - .filter(community_person_ban::community_id.eq(community::id)) - .filter(community_person_ban::person_id.eq(person::id)), - ))) + .filter(not(exists(find_action( + community_actions::received_ban, + (person::id, community::id), + )))) .select((post::all_columns, community::all_columns)) .get_results::<(Post, Community)>(&mut conn) .await @@ -551,7 +568,7 @@ async fn build_update_instance_form( // Activitypub). That's why we always need to mark instances as updated if they are // alive. let mut instance_form = InstanceForm { - updated: Some(naive_now()), + updated: Some(Utc::now()), ..InstanceForm::new(domain.to_string()) };