diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index 3d3caa261..da0d78b14 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -14,7 +14,7 @@ body: label: Requirements description: Before you create a bug report please do the following. options: - - label: Is this a bug report? For questions or discussions use https://lemmy.ml/c/lemmy_support + - label: Is this a bug report? For questions or discussions use https://lemmy.ml/c/lemmy_support or the [matrix chat](https://matrix.to/#/#lemmy:matrix.org). required: true - label: Did you check to see if this issue already exists? required: true diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml index f50a93ff2..a9a6517c5 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -12,7 +12,7 @@ body: label: Requirements description: Before you create a bug report please do the following. options: - - label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support + - label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support or the [matrix chat](https://matrix.to/#/#lemmy:matrix.org). required: true - label: Did you check to see if this issue already exists? required: true diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yml b/.github/ISSUE_TEMPLATE/QUESTION.yml index d9a9badb7..0d51c4c74 100644 --- a/.github/ISSUE_TEMPLATE/QUESTION.yml +++ b/.github/ISSUE_TEMPLATE/QUESTION.yml @@ -6,7 +6,9 @@ body: - type: markdown attributes: value: | - Have a question about Lemmy? + For questions or discussions use https://lemmy.ml/c/lemmy_support or the [matrix chat](https://matrix.to/#/#lemmy:matrix.org). + + Have a question about how Lemmy works? Please check the docs first: https://join-lemmy.org/docs/en/index.html - type: textarea id: question diff --git a/.woodpecker.yml b/.woodpecker.yml index fe549aa8a..50ed034c1 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,13 +6,13 @@ variables: # as well. Otherwise release builds can fail if Lemmy or dependencies rely on new Rust # features. In particular the ARM builder image needs to be updated manually in the repo below: # https://github.com/raskyld/lemmy-cross-toolchains - - &rust_image "rust:1.83" + - &rust_image "rust:1.81" - &rust_nightly_image "rustlang/rust:nightly" - - &install_pnpm "corepack enable pnpm" + - &install_pnpm "npm install -g corepack@latest && corepack enable pnpm" - &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin" - install_diesel_cli: &install_diesel_cli - apt-get update && apt-get install -y postgresql-client - - cargo install diesel_cli --no-default-features --features postgres + - cargo install --locked diesel_cli --no-default-features --features postgres - export PATH="$CARGO_HOME/bin:$PATH" - &slow_check_paths - event: pull_request diff --git a/Cargo.lock b/Cargo.lock index c66b34036..82c6d9b0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772" [[package]] name = "activitypub_federation" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee819cada736b6e26c59706f9e6ff89a48060e635c0546ff984d84baefc8c13a" +checksum = "cd8c76cad52a3d0f637f1f4ba06d96ac63c92512082f6a1ca86145b66a0a5371" dependencies = [ "activitystreams-kinds", "actix-web", @@ -34,7 +34,7 @@ dependencies = [ "moka", "once_cell", "pin-project-lite", - "rand", + "rand 0.8.5", "regex", "reqwest 0.12.12", "reqwest-middleware", @@ -121,7 +121,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "sha1", "smallvec", "tokio", @@ -333,10 +333,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" dependencies = [ "backtrace", ] @@ -484,9 +484,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", @@ -584,13 +584,13 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bcrypt" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b1866ecef4f2d06a0bb77880015fdf2b89e25a1c2e5addacb87e459c86dc67e" +checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" dependencies = [ "base64 0.22.1", "blowfish", - "getrandom", + "getrandom 0.3.1", "subtle", "zeroize", ] @@ -737,7 +737,7 @@ dependencies = [ "hound", "image", "lodepng", - "rand", + "rand 0.8.5", "serde_json", ] @@ -786,9 +786,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", @@ -796,7 +796,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -832,9 +832,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.26" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" dependencies = [ "clap_builder", "clap_derive", @@ -842,9 +842,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.26" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" dependencies = [ "anstream", "anstyle", @@ -854,9 +854,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.24" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1122,9 +1122,9 @@ dependencies = [ [[package]] name = "deadpool" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" dependencies = [ "deadpool-runtime", "num_cpus", @@ -1251,9 +1251,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.2.6" +version = "2.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12" +checksum = "04001f23ba8843dc315804fa324000376084dfb1c30794ff68dd279e6e5696d5" dependencies = [ "bitflags 2.8.0", "byteorder", @@ -1280,15 +1280,6 @@ 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" @@ -1541,7 +1532,7 @@ checksum = "2e1f6c3800b304a6be0012039e2a45a322a093539c45ab818d9e6895a39c90fe" dependencies = [ "proc-macro2", "quote", - "rand", + "rand 0.8.5", "syn 1.0.109", ] @@ -1787,16 +1778,48 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "glob" version = "0.3.2" @@ -1917,14 +1940,14 @@ dependencies = [ [[package]] name = "html2text" -version = "0.12.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "042a9677c258ac2952dd026bb0cd21972f00f644a5a38f5a215cb22cdaf6834e" +checksum = "eacd0d94e37b02109daef505556923edda7785047f24d9634b84835a8122da7a" dependencies = [ - "html5ever 0.27.0", - "markup5ever 0.12.1", + "html5ever 0.29.0", + "markup5ever 0.14.0", "tendril", - "thiserror 1.0.69", + "thiserror 2.0.11", "unicode-width", ] @@ -1956,6 +1979,20 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "html5ever" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "http" version = "0.2.12" @@ -2052,9 +2089,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -2101,7 +2138,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.30", + "hyper 0.14.32", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -2117,7 +2154,7 @@ dependencies = [ "http 1.2.0", "hyper 1.4.1", "hyper-util", - "rustls 0.23.21", + "rustls 0.23.23", "rustls-pki-types", "tokio", "tokio-rustls 0.26.1", @@ -2385,9 +2422,9 @@ dependencies = [ [[package]] name = "infer" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" dependencies = [ "cfb", ] @@ -2487,11 +2524,11 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.0" +version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -2523,11 +2560,10 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lemmy_api" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "activitypub_federation", "actix-web", - "actix-web-httpauth", "anyhow", "base64 0.22.1", "bcrypt", @@ -2539,10 +2575,9 @@ dependencies = [ "lemmy_api_crud", "lemmy_db_schema", "lemmy_db_views", - "lemmy_db_views_actor", - "lemmy_db_views_moderator", "lemmy_utils", "pretty_assertions", + "regex", "serial_test", "sitemap-rs", "tokio", @@ -2553,10 +2588,11 @@ dependencies = [ [[package]] name = "lemmy_api_common" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "activitypub_federation", "actix-web", + "actix-web-httpauth", "anyhow", "chrono", "encoding_rs", @@ -2566,8 +2602,6 @@ dependencies = [ "jsonwebtoken", "lemmy_db_schema", "lemmy_db_views", - "lemmy_db_views_actor", - "lemmy_db_views_moderator", "lemmy_utils", "mime", "mime_guess", @@ -2576,7 +2610,6 @@ dependencies = [ "regex", "reqwest 0.12.12", "reqwest-middleware", - "rosetta-i18n", "serde", "serde_with", "serial_test", @@ -2586,12 +2619,13 @@ dependencies = [ "url", "urlencoding", "uuid", + "webmention", "webpage", ] [[package]] name = "lemmy_api_crud" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "accept-language", "activitypub_federation", @@ -2603,21 +2637,18 @@ dependencies = [ "lemmy_api_common", "lemmy_db_schema", "lemmy_db_views", - "lemmy_db_views_actor", "lemmy_utils", "regex", "serde", "serde_json", "serde_with", - "tracing", "url", "uuid", - "webmention", ] [[package]] name = "lemmy_apub" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "activitypub_federation", "actix-web", @@ -2634,7 +2665,6 @@ dependencies = [ "lemmy_api_common", "lemmy_db_schema", "lemmy_db_views", - "lemmy_db_views_actor", "lemmy_utils", "moka", "pretty_assertions", @@ -2654,7 +2684,7 @@ dependencies = [ [[package]] name = "lemmy_db_perf" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "anyhow", "clap", @@ -2669,18 +2699,16 @@ dependencies = [ [[package]] name = "lemmy_db_schema" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "activitypub_federation", "anyhow", - "async-trait", "bcrypt", "chrono", "deadpool", "derive-new", "diesel", "diesel-async", - "diesel-bind-if-some", "diesel-derive-enum", "diesel-derive-newtype", "diesel_ltree", @@ -2691,7 +2719,7 @@ dependencies = [ "lemmy_utils", "pretty_assertions", "regex", - "rustls 0.23.21", + "rustls 0.23.23", "serde", "serde_json", "serde_with", @@ -2709,7 +2737,7 @@ dependencies = [ [[package]] name = "lemmy_db_views" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "actix-web", "chrono", @@ -2732,51 +2760,13 @@ dependencies = [ "url", ] -[[package]] -name = "lemmy_db_views_actor" -version = "0.19.6-beta.7" -dependencies = [ - "chrono", - "diesel", - "diesel-async", - "i-love-jesus", - "lemmy_db_schema", - "lemmy_utils", - "pretty_assertions", - "serde", - "serde_with", - "serial_test", - "strum", - "tokio", - "ts-rs", - "url", -] - -[[package]] -name = "lemmy_db_views_moderator" -version = "0.19.6-beta.7" -dependencies = [ - "diesel", - "diesel-async", - "i-love-jesus", - "lemmy_db_schema", - "lemmy_utils", - "pretty_assertions", - "serde", - "serde_with", - "serial_test", - "tokio", - "ts-rs", -] - [[package]] name = "lemmy_federate" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "activitypub_federation", "actix-web", "anyhow", - "async-trait", "chrono", "diesel", "diesel-async", @@ -2784,7 +2774,7 @@ dependencies = [ "lemmy_api_common", "lemmy_apub", "lemmy_db_schema", - "lemmy_db_views_actor", + "lemmy_db_views", "lemmy_utils", "mockall", "moka", @@ -2802,23 +2792,31 @@ dependencies = [ [[package]] name = "lemmy_routes" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "activitypub_federation", + "actix-cors", "actix-web", + "actix-web-prom", "anyhow", "chrono", + "clokwerk", + "diesel", + "diesel-async", "futures", + "futures-util", "http 1.2.0", "lemmy_api_common", "lemmy_db_schema", "lemmy_db_views", - "lemmy_db_views_actor", "lemmy_utils", + "pretty_assertions", + "prometheus", "reqwest 0.12.12", "reqwest-middleware", "rss", "serde", + "serial_test", "tokio", "tracing", "url", @@ -2826,18 +2824,11 @@ dependencies = [ [[package]] name = "lemmy_server" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "activitypub_federation", - "actix-cors", "actix-web", - "actix-web-prom", - "chrono", "clap", - "clokwerk", - "diesel", - "diesel-async", - "futures-util", "lemmy_api", "lemmy_api_common", "lemmy_api_crud", @@ -2846,23 +2837,20 @@ dependencies = [ "lemmy_federate", "lemmy_routes", "lemmy_utils", - "pretty_assertions", - "prometheus", + "mimalloc", "reqwest-middleware", "reqwest-tracing", - "rustls 0.23.21", + "rustls 0.23.23", "serde_json", - "serial_test", "tokio", "tracing", "tracing-actix-web", "tracing-subscriber", - "url", ] [[package]] name = "lemmy_utils" -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" dependencies = [ "actix-web", "anyhow", @@ -2873,6 +2861,7 @@ dependencies = [ "doku", "enum-map", "futures", + "git-version", "html2text", "http 1.2.0", "itertools 0.14.0", @@ -2903,9 +2892,9 @@ dependencies = [ [[package]] name = "lettre" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5" +checksum = "e882e1489810a45919477602194312b1a7df0e5acc30a6188be7b520268f63f8" dependencies = [ "async-trait", "base64 0.22.1", @@ -2921,7 +2910,7 @@ dependencies = [ "nom", "percent-encoding", "quoted_printable", - "rustls 0.23.21", + "rustls 0.23.23", "rustls-pemfile 2.2.0", "rustls-pki-types", "socket2", @@ -2944,7 +2933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2953,6 +2942,16 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libmimalloc-sys" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -3142,6 +3141,20 @@ dependencies = [ "tendril", ] +[[package]] +name = "markup5ever" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "markup5ever_rcdom" version = "0.2.0" @@ -3229,6 +3242,15 @@ dependencies = [ "quote", ] +[[package]] +name = "mimalloc" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -3278,7 +3300,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -3390,7 +3412,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -3567,7 +3589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ "phf_shared 0.10.0", - "rand", + "rand 0.8.5", ] [[package]] @@ -3577,7 +3599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand", + "rand 0.8.5", ] [[package]] @@ -3691,9 +3713,9 @@ checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "postgres-protocol" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" +checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" dependencies = [ "base64 0.22.1", "byteorder", @@ -3702,16 +3724,16 @@ dependencies = [ "hmac", "md-5", "memchr", - "rand", + "rand 0.9.0", "sha2", "stringprep", ] [[package]] name = "postgres-types" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" +checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" dependencies = [ "bytes", "fallible-iterator", @@ -3730,7 +3752,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -3888,7 +3910,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.21", + "rustls 0.23.23", "socket2", "thiserror 1.0.69", "tokio", @@ -3902,10 +3924,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" dependencies = [ "bytes", - "rand", + "rand 0.8.5", "ring", "rustc-hash 2.0.0", - "rustls 0.23.21", + "rustls 0.23.23", "slab", "thiserror 1.0.69", "tinyvec", @@ -3947,8 +3969,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.17", ] [[package]] @@ -3958,7 +3991,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", ] [[package]] @@ -3967,7 +4010,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.17", ] [[package]] @@ -4054,7 +4107,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.32", "hyper-rustls 0.24.2", "ipnet", "js-sys", @@ -4107,7 +4160,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.21", + "rustls 0.23.23", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -4151,7 +4204,7 @@ checksum = "73e6153390585f6961341b50e5a1931d6be6dee4292283635903c26ef9d980d2" dependencies = [ "anyhow", "async-trait", - "getrandom", + "getrandom 0.2.15", "http 1.2.0", "matchit", "reqwest 0.12.12", @@ -4176,7 +4229,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -4216,7 +4269,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -4289,9 +4342,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ "aws-lc-rs", "log", @@ -4430,9 +4483,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" @@ -4456,9 +4509,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "indexmap 2.7.0", "itoa", @@ -4596,7 +4649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -4773,18 +4826,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4924,9 +4977,9 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test-context" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6676ab8513edfd2601a108621103fdb45cac9098305ca25ec93f7023b06b05d9" +checksum = "cb69cce03e432993e2dc1f93f7899b952300fcb6dc44191a1b830b60b8c3c8aa" dependencies = [ "futures", "test-context-macros", @@ -4934,9 +4987,9 @@ dependencies = [ [[package]] name = "test-context-macros" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ea17a2dc368aeca6f554343ced1b1e31f76d63683fa8016e5844bd7a5144a1" +checksum = "97e0639209021e54dbe19cafabfc0b5574b078c37358945e6d473eabe39bb974" dependencies = [ "proc-macro2", "quote", @@ -5107,9 +5160,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" +checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" dependencies = [ "async-trait", "byteorder", @@ -5124,7 +5177,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand", + "rand 0.9.0", "socket2", "tokio", "tokio-util", @@ -5139,7 +5192,7 @@ checksum = "27d684bad428a0f2481f42241f821db42c54e2dc81d8c00db8536c506b0a0144" dependencies = [ "const-oid", "ring", - "rustls 0.23.21", + "rustls 0.23.23", "tokio", "tokio-postgres", "tokio-rustls 0.26.1", @@ -5162,7 +5215,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.21", + "rustls 0.23.23", "tokio", ] @@ -5222,7 +5275,7 @@ dependencies = [ "base32", "constant_time_eq", "hmac", - "rand", + "rand 0.8.5", "sha1", "sha2", "url", @@ -5449,9 +5502,9 @@ checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" @@ -5515,11 +5568,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" dependencies = [ - "getrandom", + "getrandom 0.3.1", "serde", ] @@ -5566,6 +5619,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -5753,7 +5815,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -5816,6 +5878,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-registry" version = "0.2.0" @@ -6013,6 +6081,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -6111,7 +6188,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" +dependencies = [ + "zerocopy-derive 0.8.17", ] [[package]] @@ -6125,6 +6211,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 3c83037a5..ce0aa0286 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.19.6-beta.7" +version = "1.0.0-alpha.2" edition = "2021" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -36,7 +36,6 @@ codegen-units = 1 # Reduce parallel code generation. debug = 0 [features] -json-log = ["tracing-subscriber/json"] default = [] [workspace] @@ -49,8 +48,6 @@ members = [ "crates/db_perf", "crates/db_schema", "crates/db_views", - "crates/db_views_actor", - "crates/db_views_actor", "crates/routes", "crates/federate", ] @@ -82,21 +79,19 @@ map_err_ignore = "deny" expect_used = "deny" [workspace.dependencies] -lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" } -lemmy_api_crud = { version = "=0.19.6-beta.7", path = "./crates/api_crud" } -lemmy_apub = { version = "=0.19.6-beta.7", path = "./crates/apub" } -lemmy_utils = { version = "=0.19.6-beta.7", path = "./crates/utils", default-features = false } -lemmy_db_schema = { version = "=0.19.6-beta.7", path = "./crates/db_schema" } -lemmy_api_common = { version = "=0.19.6-beta.7", path = "./crates/api_common" } -lemmy_routes = { version = "=0.19.6-beta.7", path = "./crates/routes" } -lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" } -lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" } -lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" } -lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" } -activitypub_federation = { version = "0.6.1", default-features = false, features = [ +lemmy_api = { version = "=1.0.0-alpha.2", path = "./crates/api" } +lemmy_api_crud = { version = "=1.0.0-alpha.2", path = "./crates/api_crud" } +lemmy_apub = { version = "=1.0.0-alpha.2", path = "./crates/apub" } +lemmy_utils = { version = "=1.0.0-alpha.2", path = "./crates/utils", default-features = false } +lemmy_db_schema = { version = "=1.0.0-alpha.2", path = "./crates/db_schema" } +lemmy_api_common = { version = "=1.0.0-alpha.2", path = "./crates/api_common" } +lemmy_routes = { version = "=1.0.0-alpha.2", path = "./crates/routes" } +lemmy_db_views = { version = "=1.0.0-alpha.2", path = "./crates/db_views" } +lemmy_federate = { version = "=1.0.0-alpha.2", path = "./crates/federate" } +activitypub_federation = { version = "0.6.3", default-features = false, features = [ "actix-web", ] } -diesel = "2.2.6" +diesel = "2.2.7" diesel_migrations = "2.2.0" diesel-async = "0.5.2" serde = { version = "1.0.217", features = ["derive"] } @@ -109,9 +104,9 @@ actix-web = { version = "4.9.0", default-features = false, features = [ "macros", "rustls-0_23", ] } -tracing = "0.1.41" +tracing = { version = "0.1.41", default-features = false } tracing-actix-web = { version = "0.7.15", default-features = false } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } url = { version = "2.5.4", features = ["serde"] } reqwest = { version = "0.12.12", default-features = false, features = [ "blocking", @@ -123,15 +118,14 @@ reqwest-middleware = "0.3.3" reqwest-tracing = "0.5.5" clokwerk = "0.4.0" doku = { version = "0.21.1", features = ["url-2"] } -bcrypt = "0.16.0" +bcrypt = "0.17.0" chrono = { version = "0.4.39", features = [ "now", "serde", ], default-features = false } -serde_json = { version = "1.0.135", features = ["preserve_order"] } +serde_json = { version = "1.0.138", features = ["preserve_order"] } base64 = "0.22.1" -uuid = { version = "1.12.0", features = ["serde"] } -async-trait = "0.1.85" +uuid = { version = "1.13.1", features = ["serde"] } captcha = "0.0.9" anyhow = { version = "1.0.95", features = ["backtrace"] } diesel_ltree = "0.4.0" @@ -140,7 +134,8 @@ tokio = { version = "1.43.0", 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"] } +enum-map = { version = "2.7" } +strum = { version = "0.27.0", features = ["derive"] } itertools = "0.14.0" futures = "0.3.31" http = "1.2" @@ -150,18 +145,16 @@ ts-rs = { version = "10.1.0", features = [ "no-serde-warnings", "url-impl", ] } -rustls = { version = "0.23.21", features = ["ring"] } +rustls = { version = "0.23.23", features = ["ring"] } futures-util = "0.3.31" -tokio-postgres = "0.7.12" +tokio-postgres = "0.7.13" tokio-postgres-rustls = "0.13.0" urlencoding = "2.1.3" -enum-map = "2.7" moka = { version = "0.12.10", features = ["future"] } i-love-jesus = { version = "0.1.0" } -clap = { version = "4.5.26", features = ["derive", "env"] } +clap = { version = "4.5.29", 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] @@ -174,26 +167,19 @@ lemmy_api_common = { workspace = true } lemmy_routes = { workspace = true } lemmy_federate = { workspace = true } activitypub_federation = { workspace = true } -diesel = { workspace = true } -diesel-async = { workspace = true } actix-web = { workspace = true } tracing = { workspace = true } tracing-actix-web = { workspace = true } tracing-subscriber = { workspace = true } -url = { workspace = true } reqwest-middleware = { workspace = true } reqwest-tracing = { workspace = true } -clokwerk = { workspace = true } serde_json = { workspace = true } rustls = { workspace = true } tokio.workspace = true -actix-cors = "0.7.0" -futures-util = { workspace = true } -chrono = { workspace = true } -prometheus = { version = "0.13.4", features = ["process"] } -serial_test = { workspace = true } clap = { workspace = true } -actix-web-prom = "0.9.0" +mimalloc = "0.1.43" -[dev-dependencies] -pretty_assertions = { workspace = true } +# Speedup RSA key generation +# https://github.com/RustCrypto/RSA/blob/master/README.md#example +[profile.dev.package.num-bigint-dig] +opt-level = 3 diff --git a/api_tests/package.json b/api_tests/package.json index f9c67eea7..8deccd827 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -6,7 +6,7 @@ "repository": "https://github.com/LemmyNet/lemmy", "author": "Dessalines", "license": "AGPL-3.0", - "packageManager": "pnpm@9.15.0", + "packageManager": "pnpm@10.2.1+sha512.398035c7bd696d0ba0b10a688ed558285329d27ea994804a52bad9167d8e3a72bcb993f9699585d3ca25779ac64949ef422757a6c31102c12ab932e5cbe5cc92", "scripts": { "lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'", "fix": "prettier --write src && eslint --fix src", @@ -22,16 +22,18 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/node": "^22.10.6", - "@typescript-eslint/eslint-plugin": "^8.20.0", - "@typescript-eslint/parser": "^8.20.0", - "eslint": "^9.18.0", - "eslint-plugin-prettier": "^5.1.3", + "@types/joi": "^17.2.3", + "@types/node": "^22.13.1", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", + "eslint": "^9.20.0", + "eslint-plugin-prettier": "^5.2.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-inbox-combined.1", - "prettier": "^3.4.2", + "lemmy-js-client": "0.20.0-remove-aggregate-tables.5", + "prettier": "^3.5.0", "ts-jest": "^29.1.0", + "tsoa": "^6.6.0", "typescript": "^5.7.3", - "typescript-eslint": "^8.20.0" + "typescript-eslint": "^8.24.0" } } diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 0a662e576..efd592d47 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -11,39 +11,45 @@ importers: '@types/jest': specifier: ^29.5.12 version: 29.5.14 + '@types/joi': + specifier: ^17.2.3 + version: 17.2.3 '@types/node': - specifier: ^22.10.6 - version: 22.10.6 + specifier: ^22.13.1 + version: 22.13.1 '@typescript-eslint/eslint-plugin': - specifier: ^8.20.0 - version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3) + specifier: ^8.24.0 + version: 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.0)(typescript@5.7.3))(eslint@9.20.0)(typescript@5.7.3) '@typescript-eslint/parser': - specifier: ^8.20.0 - version: 8.20.0(eslint@9.18.0)(typescript@5.7.3) + specifier: ^8.24.0 + version: 8.24.0(eslint@9.20.0)(typescript@5.7.3) eslint: - specifier: ^9.18.0 - version: 9.18.0 + specifier: ^9.20.0 + version: 9.20.0 eslint-plugin-prettier: - specifier: ^5.1.3 - version: 5.2.1(eslint@9.18.0)(prettier@3.4.2) + specifier: ^5.2.3 + version: 5.2.3(eslint@9.20.0)(prettier@3.5.0) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@22.10.6) + version: 29.7.0(@types/node@22.13.1) lemmy-js-client: - specifier: 0.20.0-inbox-combined.1 - version: 0.20.0-inbox-combined.1 + specifier: 0.20.0-remove-aggregate-tables.5 + version: 0.20.0-remove-aggregate-tables.5 prettier: - specifier: ^3.4.2 - version: 3.4.2 + specifier: ^3.5.0 + version: 3.5.0 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.10.6))(typescript@5.7.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.13.1))(typescript@5.7.3) + tsoa: + specifier: ^6.6.0 + version: 6.6.0 typescript: specifier: ^5.7.3 version: 5.7.3 typescript-eslint: - specifier: ^8.20.0 - version: 8.20.0(eslint@9.18.0)(typescript@5.7.3) + specifier: ^8.24.0 + version: 8.24.0(eslint@9.20.0)(typescript@5.7.3) packages: @@ -236,12 +242,16 @@ packages: resolution: {integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.11.0': + resolution: {integrity: sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.2.0': resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.18.0': - resolution: {integrity: sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==} + '@eslint/js@9.20.0': + resolution: {integrity: sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.5': @@ -252,6 +262,103 @@ packages: resolution: {integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hapi/accept@6.0.3': + resolution: {integrity: sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==} + + '@hapi/ammo@6.0.1': + resolution: {integrity: sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==} + + '@hapi/b64@6.0.1': + resolution: {integrity: sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==} + + '@hapi/boom@10.0.1': + resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==} + + '@hapi/bounce@3.0.2': + resolution: {integrity: sha512-d0XmlTi3H9HFDHhQLjg4F4auL1EY3Wqj7j7/hGDhFFe6xAbnm3qiGrXeT93zZnPH8gH+SKAFYiRzu26xkXcH3g==} + + '@hapi/bourne@3.0.0': + resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + + '@hapi/call@9.0.1': + resolution: {integrity: sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==} + + '@hapi/catbox-memory@6.0.2': + resolution: {integrity: sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==} + + '@hapi/catbox@12.1.1': + resolution: {integrity: sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==} + + '@hapi/content@6.0.0': + resolution: {integrity: sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==} + + '@hapi/cryptiles@6.0.1': + resolution: {integrity: sha512-9GM9ECEHfR8lk5ASOKG4+4ZsEzFqLfhiryIJ2ISePVB92OHLp/yne4m+zn7z9dgvM98TLpiFebjDFQ0UHcqxXQ==} + engines: {node: '>=14.0.0'} + + '@hapi/file@3.0.0': + resolution: {integrity: sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==} + + '@hapi/hapi@21.3.12': + resolution: {integrity: sha512-GCUP12dkb3QMjpFl+wEFO73nqKRmsnD5um/QDOn6lj2GjGBrDXPcT194mNARO+PPNXZOR4KmvIpHt/lceUncfg==} + engines: {node: '>=14.15.0'} + + '@hapi/heavy@8.0.1': + resolution: {integrity: sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==} + + '@hapi/hoek@11.0.7': + resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/iron@7.0.1': + resolution: {integrity: sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==} + + '@hapi/mimos@7.0.1': + resolution: {integrity: sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==} + + '@hapi/nigel@5.0.1': + resolution: {integrity: sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==} + engines: {node: '>=14.0.0'} + + '@hapi/pez@6.1.0': + resolution: {integrity: sha512-+FE3sFPYuXCpuVeHQ/Qag1b45clR2o54QoonE/gKHv9gukxQ8oJJZPR7o3/ydDTK6racnCJXxOyT1T93FCJMIg==} + + '@hapi/podium@5.0.2': + resolution: {integrity: sha512-T7gf2JYHQQfEfewTQFbsaXoZxSvuXO/QBIGljucUQ/lmPnTTNAepoIKOakWNVWvo2fMEDjycu77r8k6dhreqHA==} + + '@hapi/shot@6.0.1': + resolution: {integrity: sha512-s5ynMKZXYoDd3dqPw5YTvOR/vjHvMTxc388+0qL0jZZP1+uwXuUD32o9DuuuLsmTlyXCWi02BJl1pBpwRuUrNA==} + + '@hapi/somever@4.1.1': + resolution: {integrity: sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==} + + '@hapi/statehood@8.1.1': + resolution: {integrity: sha512-YbK7PSVUA59NArAW5Np0tKRoIZ5VNYUicOk7uJmWZF6XyH5gGL+k62w77SIJb0AoAJ0QdGQMCQ/WOGL1S3Ydow==} + + '@hapi/subtext@8.1.0': + resolution: {integrity: sha512-PyaN4oSMtqPjjVxLny1k0iYg4+fwGusIhaom9B2StinBclHs7v46mIW706Y+Wo21lcgulGyXbQrmT/w4dus6ww==} + + '@hapi/teamwork@6.0.0': + resolution: {integrity: sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A==} + engines: {node: '>=14.0.0'} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@hapi/topo@6.0.2': + resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + + '@hapi/validate@2.0.1': + resolution: {integrity: sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==} + + '@hapi/vise@5.0.1': + resolution: {integrity: sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==} + + '@hapi/wreck@18.1.0': + resolution: {integrity: sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -272,6 +379,10 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -376,10 +487,23 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -389,6 +513,18 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@tsoa/cli@6.6.0': + resolution: {integrity: sha512-thSW0EiqjkF7HspcPIVIy0ZX65VqbWALHbxwl8Sk83j2kakOMq+fJvfo8FcBAWlMki+JDH7CO5iaAaSLHbeqtg==} + engines: {node: '>=18.0.0', yarn: '>=1.9.4'} + hasBin: true + + '@tsoa/runtime@6.6.0': + resolution: {integrity: sha512-+rF2gdL8CX+jQ82/IBc+MRJFNAvWPoBBl77HHJv3ESVMqbKhlhlo97JHmKyFbLcX6XOJN8zl8gfQpAEJN4SOMQ==} + engines: {node: '>=18.0.0', yarn: '>=1.9.4'} + + '@types/accepts@1.3.7': + resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -401,12 +537,36 @@ packages: '@types/babel__traverse@7.20.5': resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/content-disposition@0.5.8': + resolution: {integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==} + + '@types/cookies@0.9.0': + resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + + '@types/express@5.0.0': + resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/http-assert@1.5.6': + resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -419,11 +579,42 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/joi@17.2.3': + resolution: {integrity: sha512-dGjs/lhrWOa+eO0HwgxCSnDm5eMGCsXuvLglMghJq32F6q5LyyNuXb41DHzrg501CKNOSSAHmfB7FDGeUnDmzw==} + deprecated: This is a stub types definition. joi provides its own type definitions, so you do not need this installed. + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.10.6': - resolution: {integrity: sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==} + '@types/keygrip@1.0.6': + resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} + + '@types/koa-compose@3.2.8': + resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==} + + '@types/koa@2.15.0': + resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/multer@1.4.12': + resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==} + + '@types/node@22.13.1': + resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==} + + '@types/qs@6.9.18': + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -434,53 +625,57 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@8.20.0': - resolution: {integrity: sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==} + '@typescript-eslint/eslint-plugin@8.24.0': + resolution: {integrity: sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/parser@8.20.0': - resolution: {integrity: sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==} + '@typescript-eslint/parser@8.24.0': + resolution: {integrity: sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/scope-manager@8.20.0': - resolution: {integrity: sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==} + '@typescript-eslint/scope-manager@8.24.0': + resolution: {integrity: sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.20.0': - resolution: {integrity: sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==} + '@typescript-eslint/type-utils@8.24.0': + resolution: {integrity: sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/types@8.20.0': - resolution: {integrity: sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==} + '@typescript-eslint/types@8.24.0': + resolution: {integrity: sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.20.0': - resolution: {integrity: sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==} + '@typescript-eslint/typescript-estree@8.24.0': + resolution: {integrity: sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/utils@8.20.0': - resolution: {integrity: sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==} + '@typescript-eslint/utils@8.24.0': + resolution: {integrity: sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/visitor-keys@8.20.0': - resolution: {integrity: sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==} + '@typescript-eslint/visitor-keys@8.24.0': + resolution: {integrity: sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -502,6 +697,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -510,6 +709,10 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -520,6 +723,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -551,6 +757,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -580,6 +790,18 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -631,9 +853,24 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -647,6 +884,14 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -671,6 +916,14 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -679,6 +932,16 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -694,13 +957,39 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -709,8 +998,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-plugin-prettier@5.2.1: - resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + eslint-plugin-prettier@5.2.3: + resolution: {integrity: sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -735,8 +1024,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.18.0: - resolution: {integrity: sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==} + eslint@9.20.0: + resolution: {integrity: sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -770,6 +1059,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -782,6 +1075,10 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -798,8 +1095,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.18.0: - resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + fastq@1.19.0: + resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -819,6 +1116,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -834,6 +1135,22 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -853,10 +1170,18 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -869,6 +1194,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -881,27 +1210,52 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + hasown@2.0.0: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -926,6 +1280,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -956,6 +1314,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -983,6 +1345,9 @@ packages: resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} engines: {node: '>=8'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jake@10.9.2: resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} engines: {node: '>=10'} @@ -1117,6 +1482,9 @@ packages: node-notifier: optional: true + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1150,6 +1518,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1157,8 +1528,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-inbox-combined.1: - resolution: {integrity: sha512-sFJJePXdMHIVQwCa3fN+nIcIvfD7ZbBEZn08fmITXEA6/qbJLvZGWG/rEcRNkZM+lRKnhfrZihWKx1AHZE9wqA==} + lemmy-js-client@0.20.0-remove-aggregate-tables.5: + resolution: {integrity: sha512-A/p4LLWNiVp7fsQOctbFm/biBAunk0FIl5X79WJ/hRu/UiD1M+tCLGYPGdy308R+zscZsDNqECe1LYBenOFzOA==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -1185,6 +1556,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1198,6 +1572,21 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1205,6 +1594,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} @@ -1213,6 +1606,23 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.53.0: + resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1228,12 +1638,29 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -1248,6 +1675,14 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1279,6 +1714,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1287,6 +1725,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1302,6 +1744,13 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1325,8 +1774,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.4.2: - resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + prettier@3.5.0: + resolution: {integrity: sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==} engines: {node: '>=14'} hasBin: true @@ -1338,6 +1787,10 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1345,12 +1798,27 @@ packages: pure-rand@6.0.4: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1382,6 +1850,12 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1396,6 +1870,22 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1404,9 +1894,29 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -1428,6 +1938,10 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -1436,10 +1950,18 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -1483,12 +2005,20 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - ts-api-utils@2.0.0: - resolution: {integrity: sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-api-utils@2.0.1: + resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' + ts-deepmerge@7.0.2: + resolution: {integrity: sha512-akcpDTPuez4xzULo5NwuoKwYRtjQJ9eoNfBACiBMaXwNAx7B1PKfe5wqUFJuW5uKzQ68YjDFwPaWHDG1KnFGsA==} + engines: {node: '>=14.13.1'} + ts-jest@29.2.5: resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -1516,6 +2046,11 @@ packages: tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tsoa@6.6.0: + resolution: {integrity: sha512-7FudRojmbEpbSQ3t1pyG5EjV3scF7/X75giQt1q+tnuGjjJppB8BOEmIdCK/G8S5Dqnmpwz5Q3vxluKozpIW9A==} + engines: {node: '>=18.0.0', yarn: '>=1.9.4'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1528,8 +2063,12 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript-eslint@8.20.0: - resolution: {integrity: sha512-Kxz2QRFsgbWj6Xcftlw3Dd154b3cEPFqQC+qMZrMypSijPd4UanKKvoKDrJ4o8AIfZFKAF+7sMaEIR8mTElozA==} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript-eslint@8.24.0: + resolution: {integrity: sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1540,9 +2079,22 @@ packages: engines: {node: '>=14.17'} hasBin: true + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.0.13: resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true @@ -1552,10 +2104,22 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + v8-to-istanbul@9.2.0: resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} engines: {node: '>=10.12.0'} + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -1568,10 +2132,17 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -1586,6 +2157,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -1801,9 +2377,9 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.1(eslint@9.18.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.20.0)': dependencies: - eslint: 9.18.0 + eslint: 9.20.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -1820,6 +2396,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.11.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 @@ -1834,7 +2414,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.18.0': {} + '@eslint/js@9.20.0': {} '@eslint/object-schema@2.1.5': {} @@ -1843,6 +2423,175 @@ snapshots: '@eslint/core': 0.10.0 levn: 0.4.1 + '@hapi/accept@6.0.3': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.7 + + '@hapi/ammo@6.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/b64@6.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/boom@10.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/bounce@3.0.2': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.7 + + '@hapi/bourne@3.0.0': {} + + '@hapi/call@9.0.1': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.7 + + '@hapi/catbox-memory@6.0.2': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.7 + + '@hapi/catbox@12.1.1': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.7 + '@hapi/podium': 5.0.2 + '@hapi/validate': 2.0.1 + + '@hapi/content@6.0.0': + dependencies: + '@hapi/boom': 10.0.1 + + '@hapi/cryptiles@6.0.1': + dependencies: + '@hapi/boom': 10.0.1 + + '@hapi/file@3.0.0': {} + + '@hapi/hapi@21.3.12': + dependencies: + '@hapi/accept': 6.0.3 + '@hapi/ammo': 6.0.1 + '@hapi/boom': 10.0.1 + '@hapi/bounce': 3.0.2 + '@hapi/call': 9.0.1 + '@hapi/catbox': 12.1.1 + '@hapi/catbox-memory': 6.0.2 + '@hapi/heavy': 8.0.1 + '@hapi/hoek': 11.0.7 + '@hapi/mimos': 7.0.1 + '@hapi/podium': 5.0.2 + '@hapi/shot': 6.0.1 + '@hapi/somever': 4.1.1 + '@hapi/statehood': 8.1.1 + '@hapi/subtext': 8.1.0 + '@hapi/teamwork': 6.0.0 + '@hapi/topo': 6.0.2 + '@hapi/validate': 2.0.1 + + '@hapi/heavy@8.0.1': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.7 + '@hapi/validate': 2.0.1 + + '@hapi/hoek@11.0.7': {} + + '@hapi/hoek@9.3.0': {} + + '@hapi/iron@7.0.1': + dependencies: + '@hapi/b64': 6.0.1 + '@hapi/boom': 10.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/cryptiles': 6.0.1 + '@hapi/hoek': 11.0.7 + + '@hapi/mimos@7.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + mime-db: 1.53.0 + + '@hapi/nigel@5.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + '@hapi/vise': 5.0.1 + + '@hapi/pez@6.1.0': + dependencies: + '@hapi/b64': 6.0.1 + '@hapi/boom': 10.0.1 + '@hapi/content': 6.0.0 + '@hapi/hoek': 11.0.7 + '@hapi/nigel': 5.0.1 + + '@hapi/podium@5.0.2': + dependencies: + '@hapi/hoek': 11.0.7 + '@hapi/teamwork': 6.0.0 + '@hapi/validate': 2.0.1 + + '@hapi/shot@6.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + '@hapi/validate': 2.0.1 + + '@hapi/somever@4.1.1': + dependencies: + '@hapi/bounce': 3.0.2 + '@hapi/hoek': 11.0.7 + + '@hapi/statehood@8.1.1': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/bounce': 3.0.2 + '@hapi/bourne': 3.0.0 + '@hapi/cryptiles': 6.0.1 + '@hapi/hoek': 11.0.7 + '@hapi/iron': 7.0.1 + '@hapi/validate': 2.0.1 + + '@hapi/subtext@8.1.0': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/content': 6.0.0 + '@hapi/file': 3.0.0 + '@hapi/hoek': 11.0.7 + '@hapi/pez': 6.1.0 + '@hapi/wreck': 18.1.0 + + '@hapi/teamwork@6.0.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@hapi/topo@6.0.2': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/validate@2.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + '@hapi/topo': 6.0.2 + + '@hapi/vise@5.0.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/wreck@18.1.0': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/hoek': 11.0.7 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -1856,6 +2605,15 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -1869,7 +2627,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1882,14 +2640,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.10.6) + jest-config: 29.7.0(@types/node@22.13.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1914,7 +2672,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1932,7 +2690,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.10.6 + '@types/node': 22.13.1 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1954,7 +2712,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 22.10.6 + '@types/node': 22.13.1 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2024,7 +2782,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2055,10 +2813,21 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.18.0 + fastq: 1.19.0 + + '@pkgjs/parseargs@0.11.0': + optional: true '@pkgr/core@0.1.1': {} + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -2069,6 +2838,39 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@tsoa/cli@6.6.0': + dependencies: + '@tsoa/runtime': 6.6.0 + '@types/multer': 1.4.12 + fs-extra: 11.3.0 + glob: 10.4.5 + handlebars: 4.7.8 + merge-anything: 5.1.7 + minimatch: 9.0.5 + ts-deepmerge: 7.0.2 + typescript: 5.7.3 + validator: 13.12.0 + yaml: 2.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@tsoa/runtime@6.6.0': + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hapi': 21.3.12 + '@types/koa': 2.15.0 + '@types/multer': 1.4.12 + express: 4.21.2 + reflect-metadata: 0.2.2 + validator: 13.12.0 + transitivePeerDependencies: + - supports-color + + '@types/accepts@1.3.7': + dependencies: + '@types/node': 22.13.1 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.23.9 @@ -2090,11 +2892,47 @@ snapshots: dependencies: '@babel/types': 7.23.9 + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.13.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.13.1 + + '@types/content-disposition@0.5.8': {} + + '@types/cookies@0.9.0': + dependencies: + '@types/connect': 3.4.38 + '@types/express': 5.0.0 + '@types/keygrip': 1.0.6 + '@types/node': 22.13.1 + '@types/estree@1.0.6': {} + '@types/express-serve-static-core@5.0.6': + dependencies: + '@types/node': 22.13.1 + '@types/qs': 6.9.18 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@5.0.0': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.6 + '@types/qs': 6.9.18 + '@types/serve-static': 1.15.7 + '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 + + '@types/http-assert@1.5.6': {} + + '@types/http-errors@2.0.4': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -2111,12 +2949,54 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/joi@17.2.3': + dependencies: + joi: 17.13.3 + '@types/json-schema@7.0.15': {} - '@types/node@22.10.6': + '@types/keygrip@1.0.6': {} + + '@types/koa-compose@3.2.8': + dependencies: + '@types/koa': 2.15.0 + + '@types/koa@2.15.0': + dependencies: + '@types/accepts': 1.3.7 + '@types/content-disposition': 0.5.8 + '@types/cookies': 0.9.0 + '@types/http-assert': 1.5.6 + '@types/http-errors': 2.0.4 + '@types/keygrip': 1.0.6 + '@types/koa-compose': 3.2.8 + '@types/node': 22.13.1 + + '@types/mime@1.3.5': {} + + '@types/multer@1.4.12': + dependencies: + '@types/express': 5.0.0 + + '@types/node@22.13.1': dependencies: undici-types: 6.20.0 + '@types/qs@6.9.18': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.13.1 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.13.1 + '@types/send': 0.17.4 + '@types/stack-utils@2.0.3': {} '@types/yargs-parser@21.0.3': {} @@ -2125,83 +3005,88 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.0)(typescript@5.7.3))(eslint@9.20.0)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.20.0(eslint@9.18.0)(typescript@5.7.3) - '@typescript-eslint/scope-manager': 8.20.0 - '@typescript-eslint/type-utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3) - '@typescript-eslint/utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3) - '@typescript-eslint/visitor-keys': 8.20.0 - eslint: 9.18.0 + '@typescript-eslint/parser': 8.24.0(eslint@9.20.0)(typescript@5.7.3) + '@typescript-eslint/scope-manager': 8.24.0 + '@typescript-eslint/type-utils': 8.24.0(eslint@9.20.0)(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.0(eslint@9.20.0)(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.24.0 + eslint: 9.20.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.0.0(typescript@5.7.3) + ts-api-utils: 2.0.1(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3)': + '@typescript-eslint/parser@8.24.0(eslint@9.20.0)(typescript@5.7.3)': dependencies: - '@typescript-eslint/scope-manager': 8.20.0 - '@typescript-eslint/types': 8.20.0 - '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3) - '@typescript-eslint/visitor-keys': 8.20.0 + '@typescript-eslint/scope-manager': 8.24.0 + '@typescript-eslint/types': 8.24.0 + '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.24.0 debug: 4.4.0 - eslint: 9.18.0 + eslint: 9.20.0 typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.20.0': + '@typescript-eslint/scope-manager@8.24.0': dependencies: - '@typescript-eslint/types': 8.20.0 - '@typescript-eslint/visitor-keys': 8.20.0 + '@typescript-eslint/types': 8.24.0 + '@typescript-eslint/visitor-keys': 8.24.0 - '@typescript-eslint/type-utils@8.20.0(eslint@9.18.0)(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.24.0(eslint@9.20.0)(typescript@5.7.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3) - '@typescript-eslint/utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.0(eslint@9.20.0)(typescript@5.7.3) debug: 4.4.0 - eslint: 9.18.0 - ts-api-utils: 2.0.0(typescript@5.7.3) + eslint: 9.20.0 + ts-api-utils: 2.0.1(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.20.0': {} + '@typescript-eslint/types@8.24.0': {} - '@typescript-eslint/typescript-estree@8.20.0(typescript@5.7.3)': + '@typescript-eslint/typescript-estree@8.24.0(typescript@5.7.3)': dependencies: - '@typescript-eslint/types': 8.20.0 - '@typescript-eslint/visitor-keys': 8.20.0 + '@typescript-eslint/types': 8.24.0 + '@typescript-eslint/visitor-keys': 8.24.0 debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 - ts-api-utils: 2.0.0(typescript@5.7.3) + semver: 7.7.1 + ts-api-utils: 2.0.1(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.20.0(eslint@9.18.0)(typescript@5.7.3)': + '@typescript-eslint/utils@8.24.0(eslint@9.20.0)(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0) - '@typescript-eslint/scope-manager': 8.20.0 - '@typescript-eslint/types': 8.20.0 - '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3) - eslint: 9.18.0 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.0) + '@typescript-eslint/scope-manager': 8.24.0 + '@typescript-eslint/types': 8.24.0 + '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) + eslint: 9.20.0 typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.20.0': + '@typescript-eslint/visitor-keys@8.24.0': dependencies: - '@typescript-eslint/types': 8.20.0 + '@typescript-eslint/types': 8.24.0 eslint-visitor-keys: 4.2.0 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -2221,12 +3106,16 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.1.0: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2238,6 +3127,8 @@ snapshots: argparse@2.0.1: {} + array-flatten@1.1.1: {} + async@3.2.6: {} babel-jest@29.7.0(@babel/core@7.23.9): @@ -2294,6 +3185,23 @@ snapshots: balanced-match@1.0.2: {} + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -2328,6 +3236,18 @@ snapshots: buffer-from@1.1.2: {} + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.1: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.7 + callsites@3.1.0: {} camelcase@5.3.1: {} @@ -2365,15 +3285,25 @@ snapshots: concat-map@0.0.1: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@22.10.6): + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + create-jest@29.7.0(@types/node@22.13.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.10.6) + jest-config: 29.7.0(@types/node@22.13.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2394,6 +3324,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.0: dependencies: ms: 2.1.3 @@ -2404,10 +3338,24 @@ snapshots: deepmerge@4.3.1: {} + depd@2.0.0: {} + + destroy@1.2.0: {} + detect-newline@3.1.0: {} diff-sequences@29.6.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + ejs@3.1.10: dependencies: jake: 10.9.2 @@ -2418,20 +3366,36 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + escalade@3.1.1: {} + escape-html@1.0.3: {} + escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.2.1(eslint@9.18.0)(prettier@3.4.2): + eslint-plugin-prettier@5.2.3(eslint@9.20.0)(prettier@3.5.0): dependencies: - eslint: 9.18.0 - prettier: 3.4.2 + eslint: 9.20.0 + prettier: 3.5.0 prettier-linter-helpers: 1.0.0 synckit: 0.9.1 @@ -2444,14 +3408,14 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.18.0: + eslint@9.20.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.0) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.1 - '@eslint/core': 0.10.0 + '@eslint/core': 0.11.0 '@eslint/eslintrc': 3.2.0 - '@eslint/js': 9.18.0 + '@eslint/js': 9.20.0 '@eslint/plugin-kit': 0.2.5 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -2503,6 +3467,8 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.3 @@ -2525,6 +3491,42 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -2541,7 +3543,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fastq@1.18.0: + fastq@1.19.0: dependencies: reusify: 1.0.4 @@ -2565,6 +3567,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -2582,6 +3596,21 @@ snapshots: flatted@3.3.2: {} + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -2593,8 +3622,26 @@ snapshots: get-caller-file@2.0.5: {} + get-intrinsic@1.2.7: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-package-type@0.1.0: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@6.0.1: {} glob-parent@5.1.2: @@ -2605,6 +3652,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -2618,20 +3674,49 @@ snapshots: globals@14.0.0: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-flag@4.0.0: {} + has-symbols@1.1.0: {} + hasown@2.0.0: dependencies: function-bind: 1.1.2 + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + html-escaper@2.0.2: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + human-signals@2.1.0: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} import-fresh@3.3.0: @@ -2653,6 +3738,8 @@ snapshots: inherits@2.0.4: {} + ipaddr.js@1.9.1: {} + is-arrayish@0.2.1: {} is-core-module@2.13.1: @@ -2673,6 +3760,8 @@ snapshots: is-stream@2.0.1: {} + is-what@4.1.16: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -2716,6 +3805,12 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jake@10.9.2: dependencies: async: 3.2.6 @@ -2735,7 +3830,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -2755,16 +3850,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.10.6): + jest-cli@29.7.0(@types/node@22.13.1): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.10.6) + create-jest: 29.7.0(@types/node@22.13.1) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.10.6) + jest-config: 29.7.0(@types/node@22.13.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2774,7 +3869,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.10.6): + jest-config@29.7.0(@types/node@22.13.1): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 @@ -2799,7 +3894,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2828,7 +3923,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2838,7 +3933,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.10.6 + '@types/node': 22.13.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2877,7 +3972,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2912,7 +4007,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2940,7 +4035,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -2986,7 +4081,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3005,7 +4100,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.6 + '@types/node': 22.13.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3014,23 +4109,31 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.10.6): + jest@29.7.0(@types/node@22.13.1): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.10.6) + jest-cli: 29.7.0(@types/node@22.13.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -3054,13 +4157,19 @@ snapshots: json5@2.2.3: {} + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 kleur@3.0.3: {} - lemmy-js-client@0.20.0-inbox-combined.1: {} + lemmy-js-client@0.20.0-remove-aggregate-tables.5: {} leven@3.1.0: {} @@ -3083,6 +4192,8 @@ snapshots: lodash.merge@4.6.2: {} + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3097,10 +4208,22 @@ snapshots: dependencies: tmpl: 1.0.5 + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-anything@5.1.7: + dependencies: + is-what: 4.1.16 + + merge-descriptors@1.0.3: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.5: dependencies: braces: 3.0.2 @@ -3111,6 +4234,16 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-db@1.53.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + mimic-fn@2.1.0: {} minimatch@3.1.2: @@ -3125,10 +4258,20 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimist@1.2.8: {} + + minipass@7.1.2: {} + + ms@2.0.0: {} + ms@2.1.3: {} natural-compare@1.4.0: {} + negotiator@0.6.3: {} + + neo-async@2.6.2: {} + node-int64@0.4.0: {} node-releases@2.0.14: {} @@ -3139,6 +4282,12 @@ snapshots: dependencies: path-key: 3.1.1 + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3174,6 +4323,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -3185,6 +4336,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -3193,6 +4346,13 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@0.1.12: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3209,7 +4369,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.4.2: {} + prettier@3.5.0: {} pretty-format@29.7.0: dependencies: @@ -3222,14 +4382,34 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} pure-rand@6.0.4: {} + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + react-is@18.3.1: {} + reflect-metadata@0.2.2: {} + require-directory@2.1.1: {} resolve-cwd@3.0.0: @@ -3254,20 +4434,85 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + semver@6.3.1: {} semver@7.6.2: {} semver@7.6.3: {} + semver@7.7.1: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -3285,6 +4530,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + statuses@2.0.1: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -3296,10 +4543,20 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + strip-bom@4.0.0: {} strip-final-newline@2.0.0: {} @@ -3335,16 +4592,20 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@2.0.0(typescript@5.7.3): + toidentifier@1.0.1: {} + + ts-api-utils@2.0.1(typescript@5.7.3): dependencies: typescript: 5.7.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.10.6))(typescript@5.7.3): + ts-deepmerge@7.0.2: {} + + ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.13.1))(typescript@5.7.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.10.6) + jest: 29.7.0(@types/node@22.13.1) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -3360,6 +4621,13 @@ snapshots: tslib@2.6.3: {} + tsoa@6.6.0: + dependencies: + '@tsoa/cli': 6.6.0 + '@tsoa/runtime': 6.6.0 + transitivePeerDependencies: + - supports-color + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -3368,20 +4636,32 @@ snapshots: type-fest@0.21.3: {} - typescript-eslint@8.20.0(eslint@9.18.0)(typescript@5.7.3): + type-is@1.6.18: dependencies: - '@typescript-eslint/eslint-plugin': 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3) - '@typescript-eslint/parser': 8.20.0(eslint@9.18.0)(typescript@5.7.3) - '@typescript-eslint/utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3) - eslint: 9.18.0 + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript-eslint@8.24.0(eslint@9.20.0)(typescript@5.7.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.0)(typescript@5.7.3))(eslint@9.20.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.24.0(eslint@9.20.0)(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.0(eslint@9.20.0)(typescript@5.7.3) + eslint: 9.20.0 typescript: 5.7.3 transitivePeerDependencies: - supports-color typescript@5.7.3: {} + uglify-js@3.19.3: + optional: true + undici-types@6.20.0: {} + universalify@2.0.1: {} + + unpipe@1.0.0: {} + update-browserslist-db@1.0.13(browserslist@4.22.3): dependencies: browserslist: 4.22.3 @@ -3392,12 +4672,18 @@ snapshots: dependencies: punycode: 2.3.1 + utils-merge@1.0.1: {} + v8-to-istanbul@9.2.0: dependencies: '@jridgewell/trace-mapping': 0.3.22 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + validator@13.12.0: {} + + vary@1.1.2: {} + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -3408,12 +4694,20 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} write-file-atomic@4.0.2: @@ -3425,6 +4719,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.7.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh index c5151b7f5..ec492655f 100755 --- a/api_tests/prepare-drone-federation-test.sh +++ b/api_tests/prepare-drone-federation-test.sh @@ -9,17 +9,31 @@ then fi export RUST_BACKTRACE=1 -export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL" +export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL" export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min -# pictrs setup -if [ ! -f "api_tests/pict-rs" ]; then - # This one sometimes goes down - # curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o api_tests/pict-rs - curl "https://codeberg.org/asonix/pict-rs/releases/download/v0.5.6/pict-rs-linux-amd64" -o api_tests/pict-rs - chmod +x api_tests/pict-rs +PICTRS_PATH="api_tests/pict-rs" +PICTRS_EXPECTED_HASH="7f7ac2a45ef9b13403ee139b7512135be6b060ff2f6460e0c800e18e1b49d2fd api_tests/pict-rs" + +# Pictrs setup. Download file with hash check and up to 3 retries. +if [ ! -f "$PICTRS_PATH" ]; then + count=0 + while [ ! -f "$PICTRS_PATH" ] && [ "$count" -lt 3 ] + do + # This one sometimes goes down + curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.17-pre.9/pict-rs-linux-amd64" -o "$PICTRS_PATH" + # curl "https://codeberg.org/asonix/pict-rs/releases/download/v0.5.5/pict-rs-linux-amd64" -o "$PICTRS_PATH" + PICTRS_HASH=$(sha256sum "$PICTRS_PATH") + if [[ "$PICTRS_HASH" != "$PICTRS_EXPECTED_HASH" ]]; then + echo "Pictrs binary hash mismatch, was $PICTRS_HASH but expected $PICTRS_EXPECTED_HASH" + rm "$PICTRS_PATH" + let count=count+1 + fi + done + chmod +x "$PICTRS_PATH" fi + ./api_tests/pict-rs \ run -a 0.0.0.0:8080 \ --danger-dummy-mode \ @@ -72,13 +86,11 @@ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \ target/lemmy_server >$LOG_DIR/lemmy_gamma.out 2>&1 & echo "start delta" -# An instance with only an allowlist for beta LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \ target/lemmy_server >$LOG_DIR/lemmy_delta.out 2>&1 & echo "start epsilon" -# An instance who has a blocklist, with lemmy-alpha blocked LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \ target/lemmy_server >$LOG_DIR/lemmy_epsilon.out 2>&1 & diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index 42fc06882..5a532d488 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -69,7 +69,7 @@ function assertCommentFederation( expect(commentOne?.comment.ap_id).toBe(commentTwo?.comment.ap_id); expect(commentOne?.comment.content).toBe(commentTwo?.comment.content); expect(commentOne?.creator.name).toBe(commentTwo?.creator.name); - expect(commentOne?.community.actor_id).toBe(commentTwo?.community.actor_id); + expect(commentOne?.community.ap_id).toBe(commentTwo?.community.ap_id); expect(commentOne?.comment.published).toBe(commentTwo?.comment.published); expect(commentOne?.comment.updated).toBe(commentOne?.comment.updated); expect(commentOne?.comment.deleted).toBe(commentOne?.comment.deleted); @@ -81,19 +81,19 @@ test("Create a comment", async () => { expect(commentRes.comment_view.comment.content).toBeDefined(); expect(commentRes.comment_view.community.local).toBe(false); expect(commentRes.comment_view.creator.local).toBe(true); - expect(commentRes.comment_view.counts.score).toBe(1); + expect(commentRes.comment_view.comment.score).toBe(1); // Make sure that comment is liked on beta let betaComment = ( await waitUntil( () => resolveComment(beta, commentRes.comment_view.comment), - c => c.comment?.counts.score === 1, + c => c.comment?.comment.score === 1, ) ).comment; expect(betaComment).toBeDefined(); expect(betaComment?.community.local).toBe(true); expect(betaComment?.creator.local).toBe(false); - expect(betaComment?.counts.score).toBe(1); + expect(betaComment?.comment.score).toBe(1); assertCommentFederation(betaComment, commentRes.comment_view); }); @@ -293,48 +293,48 @@ test("Unlike a comment", async () => { let gammaComment1 = ( await waitUntil( () => resolveComment(gamma, commentRes.comment_view.comment), - c => c.comment?.counts.score === 1, + c => c.comment?.comment.score === 1, ) ).comment; expect(gammaComment1).toBeDefined(); expect(gammaComment1?.community.local).toBe(false); expect(gammaComment1?.creator.local).toBe(false); - expect(gammaComment1?.counts.score).toBe(1); + expect(gammaComment1?.comment.score).toBe(1); let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment); - expect(unlike.comment_view.counts.score).toBe(0); + expect(unlike.comment_view.comment.score).toBe(0); // Make sure that comment is unliked on beta let betaComment = ( await waitUntil( () => resolveComment(beta, commentRes.comment_view.comment), - c => c.comment?.counts.score === 0, + c => c.comment?.comment.score === 0, ) ).comment; expect(betaComment).toBeDefined(); expect(betaComment?.community.local).toBe(true); expect(betaComment?.creator.local).toBe(false); - expect(betaComment?.counts.score).toBe(0); + expect(betaComment?.comment.score).toBe(0); // Make sure that comment is unliked on gamma, downstream peer // This is testing replication from remote-home-remote (alpha-beta-gamma) let gammaComment = ( await waitUntil( () => resolveComment(gamma, commentRes.comment_view.comment), - c => c.comment?.counts.score === 0, + c => c.comment?.comment.score === 0, ) ).comment; expect(gammaComment).toBeDefined(); expect(gammaComment?.community.local).toBe(false); expect(gammaComment?.creator.local).toBe(false); - expect(gammaComment?.counts.score).toBe(0); + expect(gammaComment?.comment.score).toBe(0); }); test("Federated comment like", async () => { let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id); await waitUntil( () => resolveComment(beta, commentRes.comment_view.comment), - c => c.comment?.counts.score === 1, + c => c.comment?.comment.score === 1, ); // Find the comment on beta let betaComment = ( @@ -346,14 +346,14 @@ test("Federated comment like", async () => { } let like = await likeComment(beta, 1, betaComment.comment); - expect(like.comment_view.counts.score).toBe(2); + expect(like.comment_view.comment.score).toBe(2); // Get the post from alpha, check the likes let postComments = await waitUntil( () => getComments(alpha, postOnAlphaRes.post_view.post.id), - c => c.comments[0].counts.score === 2, + c => c.comments[0].comment.score === 2, ); - expect(postComments.comments[0].counts.score).toBe(2); + expect(postComments.comments[0].comment.score).toBe(2); }); test("Reply to a comment from another instance, get notification", async () => { @@ -377,7 +377,7 @@ test("Reply to a comment from another instance, get notification", async () => { let betaComment = ( await waitUntil( () => resolveComment(beta, commentRes.comment_view.comment), - c => c.comment?.counts.score === 1, + c => c.comment?.comment.score === 1, ) ).comment; @@ -397,12 +397,12 @@ test("Reply to a comment from another instance, get notification", async () => { expect(getCommentParentId(replyRes.comment_view.comment)).toBe( betaComment.comment.id, ); - expect(replyRes.comment_view.counts.score).toBe(1); + expect(replyRes.comment_view.comment.score).toBe(1); // Make sure that reply comment is seen on alpha let commentSearch = await waitUntil( () => resolveComment(alpha, replyRes.comment_view.comment), - c => c.comment?.counts.score === 1, + c => c.comment?.comment.score === 1, ); let alphaComment = commentSearch.comment!; let postComments = await waitUntil( @@ -418,7 +418,7 @@ test("Reply to a comment from another instance, get notification", async () => { ); expect(alphaComment.community.local).toBe(false); expect(alphaComment.creator.local).toBe(false); - expect(alphaComment.counts.score).toBe(1); + expect(alphaComment.comment.score).toBe(1); assertCommentFederation(alphaComment, replyRes.comment_view); // Did alpha get notified of the reply from beta? @@ -441,7 +441,7 @@ test("Reply to a comment from another instance, get notification", async () => { expect(alphaReply.comment.content).toBeDefined(); expect(alphaReply.community.local).toBe(false); expect(alphaReply.creator.local).toBe(false); - expect(alphaReply.counts.score).toBe(1); + expect(alphaReply.comment.score).toBe(1); // ToDo: interesting alphaRepliesRes.replies[0].comment_reply.id is 1, meaning? how did that come about? expect(alphaReply.comment.id).toBe(alphaComment.comment.id); // this is a new notification, getReplies fetch was for read/unread both, confirm it is unread. @@ -515,7 +515,7 @@ test("Mention beta from alpha comment", async () => { expect(mentionRes.comment_view.comment.content).toBeDefined(); expect(mentionRes.comment_view.community.local).toBe(false); expect(mentionRes.comment_view.creator.local).toBe(true); - expect(mentionRes.comment_view.counts.score).toBe(1); + expect(mentionRes.comment_view.comment.score).toBe(1); // get beta's localized copy of the alpha post let betaPost = await waitForPost(beta, postOnAlphaRes.post_view.post); @@ -528,7 +528,7 @@ test("Mention beta from alpha comment", async () => { // Make sure that both new comments are seen on beta and have parent/child relationship let betaPostComments = await waitUntil( () => getComments(beta, betaPost!.post.id), - c => c.comments[1]?.counts.score === 1, + c => c.comments[1]?.comment.score === 1, ); expect(betaPostComments.comments.length).toEqual(2); // the trunk-branch root comment will be older than the mention reply comment, so index 1 @@ -542,7 +542,7 @@ test("Mention beta from alpha comment", async () => { ); expect(betaRootComment.community.local).toBe(true); expect(betaRootComment.creator.local).toBe(false); - expect(betaRootComment.counts.score).toBe(1); + expect(betaRootComment.comment.score).toBe(1); assertCommentFederation(betaRootComment, commentRes.comment_view); let mentionsRes = await waitUntil( @@ -554,7 +554,7 @@ test("Mention beta from alpha comment", async () => { expect(firstMention.comment.content).toBeDefined(); expect(firstMention.community.local).toBe(true); expect(firstMention.creator.local).toBe(false); - expect(firstMention.counts.score).toBe(1); + expect(firstMention.comment.score).toBe(1); // the reply comment with mention should be the most fresh, newest, index 0 expect(firstMention.person_comment_mention.comment_id).toBe( betaPostComments.comments[0].comment.id, @@ -581,7 +581,7 @@ test("A and G subscribe to B (center) A posts, G mentions B, it gets announced t // follow community from beta so that it accepts the mention let betaCommunity = await resolveCommunity( beta, - alphaCommunity.community.actor_id, + alphaCommunity.community.ap_id, ); await followCommunity(beta, true, betaCommunity.community!.community.id); @@ -606,17 +606,17 @@ test("A and G subscribe to B (center) A posts, G mentions B, it gets announced t expect(commentRes.comment_view.comment.content).toBe(commentContent); expect(commentRes.comment_view.community.local).toBe(false); expect(commentRes.comment_view.creator.local).toBe(true); - expect(commentRes.comment_view.counts.score).toBe(1); + expect(commentRes.comment_view.comment.score).toBe(1); // Make sure alpha sees it let alphaPostComments2 = await waitUntil( () => getComments(alpha, alphaPost.post_view.post.id), - e => e.comments[0]?.counts.score === 1, + e => e.comments[0]?.comment.score === 1, ); expect(alphaPostComments2.comments[0].comment.content).toBe(commentContent); expect(alphaPostComments2.comments[0].community.local).toBe(true); expect(alphaPostComments2.comments[0].creator.local).toBe(false); - expect(alphaPostComments2.comments[0].counts.score).toBe(1); + expect(alphaPostComments2.comments[0].comment.score).toBe(1); assertCommentFederation( alphaPostComments2.comments[0], commentRes.comment_view, @@ -688,17 +688,17 @@ test("Check that activity from another instance is sent to third instance", asyn expect(commentRes.comment_view.comment.content).toBe(commentContent); expect(commentRes.comment_view.community.local).toBe(false); expect(commentRes.comment_view.creator.local).toBe(true); - expect(commentRes.comment_view.counts.score).toBe(1); + expect(commentRes.comment_view.comment.score).toBe(1); // Make sure alpha sees it let alphaPostComments2 = await waitUntil( () => getComments(alpha, alphaPost!.post.id), - e => e.comments[0]?.counts.score === 1, + e => e.comments[0]?.comment.score === 1, ); expect(alphaPostComments2.comments[0].comment.content).toBe(commentContent); expect(alphaPostComments2.comments[0].community.local).toBe(false); expect(alphaPostComments2.comments[0].creator.local).toBe(false); - expect(alphaPostComments2.comments[0].counts.score).toBe(1); + expect(alphaPostComments2.comments[0].comment.score).toBe(1); assertCommentFederation( alphaPostComments2.comments[0], commentRes.comment_view, diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index 2d1570ea6..016574eaa 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -44,9 +44,7 @@ function assertCommunityFederation( communityOne?: CommunityView, communityTwo?: CommunityView, ) { - expect(communityOne?.community.actor_id).toBe( - communityTwo?.community.actor_id, - ); + expect(communityOne?.community.ap_id).toBe(communityTwo?.community.ap_id); expect(communityOne?.community.name).toBe(communityTwo?.community.name); expect(communityOne?.community.title).toBe(communityTwo?.community.title); expect(communityOne?.community.description).toBe( @@ -198,7 +196,7 @@ test("Admin actions in remote community are not federated to origin", async () = // gamma follows community and posts in it let gammaCommunity = ( - await resolveCommunity(gamma, communityRes.community.actor_id) + await resolveCommunity(gamma, communityRes.community.ap_id) ).community; if (!gammaCommunity) { throw "Missing gamma community"; @@ -206,7 +204,7 @@ test("Admin actions in remote community are not federated to origin", async () = await followCommunity(gamma, true, gammaCommunity.community.id); gammaCommunity = ( await waitUntil( - () => resolveCommunity(gamma, communityRes.community.actor_id), + () => resolveCommunity(gamma, communityRes.community.ap_id), g => g.community?.subscribed === "Subscribed", ) ).community; @@ -221,7 +219,7 @@ test("Admin actions in remote community are not federated to origin", async () = // admin of beta decides to ban gamma from community let betaCommunity = ( - await resolveCommunity(beta, communityRes.community.actor_id) + await resolveCommunity(beta, communityRes.community.ap_id) ).community; if (!betaCommunity) { throw "Missing beta community"; @@ -230,7 +228,7 @@ test("Admin actions in remote community are not federated to origin", async () = if (!bannedUserInfo1) { throw "Missing banned user 1"; } - let bannedUserInfo2 = (await resolvePerson(beta, bannedUserInfo1.actor_id)) + let bannedUserInfo2 = (await resolvePerson(beta, bannedUserInfo1.ap_id)) .person; if (!bannedUserInfo2) { throw "Missing banned user 2"; @@ -379,10 +377,11 @@ test("User blocks instance, communities are hidden", async () => { expect(listing_ids3).toContain(postRes.post_view.post.ap_id); }); -test("Community follower count is federated", async () => { +// TODO: this test keeps failing randomly in CI +test.skip("Community follower count is federated", async () => { // Follow the beta community from alpha let community = await createCommunity(beta); - let communityActorId = community.community_view.community.actor_id; + let communityActorId = community.community_view.community.ap_id; let resolved = await resolveCommunity(alpha, communityActorId); if (!resolved.community) { throw "Missing beta community"; @@ -397,7 +396,7 @@ test("Community follower count is federated", async () => { ).community; // Make sure there is 1 subscriber - expect(followed?.counts.subscribers).toBe(1); + expect(followed?.community.subscribers).toBe(1); // Follow the community from gamma resolved = await resolveCommunity(gamma, communityActorId); @@ -414,7 +413,7 @@ test("Community follower count is federated", async () => { ).community; // Make sure there are 2 subscribers - expect(followed?.counts?.subscribers).toBe(2); + expect(followed?.community?.subscribers).toBe(2); // Follow the community from delta resolved = await resolveCommunity(delta, communityActorId); @@ -431,16 +430,16 @@ test("Community follower count is federated", async () => { ).community; // Make sure there are 3 subscribers - expect(followed?.counts?.subscribers).toBe(3); + expect(followed?.community?.subscribers).toBe(3); }); test("Dont receive community activities after unsubscribe", async () => { let communityRes = await createCommunity(alpha); expect(communityRes.community_view.community.name).toBeDefined(); - expect(communityRes.community_view.counts.subscribers).toBe(1); + expect(communityRes.community_view.community.subscribers).toBe(1); let betaCommunity = ( - await resolveCommunity(beta, communityRes.community_view.community.actor_id) + await resolveCommunity(beta, communityRes.community_view.community.ap_id) ).community; assertCommunityFederation(betaCommunity, communityRes.community_view); @@ -452,7 +451,7 @@ test("Dont receive community activities after unsubscribe", async () => { alpha, communityRes.community_view.community.id, ); - expect(communityRes1.community_view.counts.subscribers).toBe(2); + expect(communityRes1.community_view.community.subscribers).toBe(2); // temporarily block alpha, so that it doesn't know about unfollow var allow_instance_params: AdminAllowInstanceParams = { @@ -471,7 +470,7 @@ test("Dont receive community activities after unsubscribe", async () => { alpha, communityRes.community_view.community.id, ); - expect(communityRes2.community_view.counts.subscribers).toBe(2); + expect(communityRes2.community_view.community.subscribers).toBe(2); // unblock alpha allow_instance_params.allow = true; @@ -487,13 +486,13 @@ test("Dont receive community activities after unsubscribe", async () => { // await longDelay(); let postResBeta = searchPostLocal(beta, postRes.post_view.post); - expect((await postResBeta).posts.length).toBe(0); + expect((await postResBeta).results.length).toBe(0); }); test("Fetch community, includes posts", async () => { let communityRes = await createCommunity(alpha); expect(communityRes.community_view.community.name).toBeDefined(); - expect(communityRes.community_view.counts.subscribers).toBe(1); + expect(communityRes.community_view.community.subscribers).toBe(1); let postRes = await createPost( alpha, @@ -502,13 +501,12 @@ test("Fetch community, includes posts", async () => { expect(postRes.post_view.post).toBeDefined(); let resolvedCommunity = await waitUntil( - () => - resolveCommunity(beta, communityRes.community_view.community.actor_id), + () => resolveCommunity(beta, communityRes.community_view.community.ap_id), c => c.community?.community.id != undefined, ); let betaCommunity = resolvedCommunity.community; - expect(betaCommunity?.community.actor_id).toBe( - communityRes.community_view.community.actor_id, + expect(betaCommunity?.community.ap_id).toBe( + communityRes.community_view.community.ap_id, ); await longDelay(); @@ -529,7 +527,7 @@ test("Content in local-only community doesn't federate", async () => { // cant resolve the community from another instance await expect( - resolveCommunity(beta, communityRes.actor_id), + resolveCommunity(beta, communityRes.ap_id), ).rejects.toStrictEqual(Error("not_found")); // create a post, also cant resolve it @@ -544,7 +542,7 @@ test("Remote mods can edit communities", async () => { let betaCommunity = await resolveCommunity( beta, - communityRes.community_view.community.actor_id, + communityRes.community_view.community.ap_id, ); if (!betaCommunity.community) { throw "Missing beta community"; @@ -583,7 +581,7 @@ test("Community name with non-ascii chars", async () => { let betaCommunity1 = await resolveCommunity( beta, - communityRes.community_view.community.actor_id, + communityRes.community_view.community.ap_id, ); expect(betaCommunity1.community!.community.name).toBe(name); diff --git a/api_tests/src/follow.spec.ts b/api_tests/src/follow.spec.ts index c447e14cd..824c34f4a 100644 --- a/api_tests/src/follow.spec.ts +++ b/api_tests/src/follow.spec.ts @@ -27,21 +27,21 @@ test("Follow local community", async () => { // Make sure the follow response went through expect(follow.community_view.community.local).toBe(true); expect(follow.community_view.subscribed).toBe("Subscribed"); - expect(follow.community_view.counts.subscribers).toBe( - community.counts.subscribers + 1, + expect(follow.community_view.community.subscribers).toBe( + community.community.subscribers + 1, ); - expect(follow.community_view.counts.subscribers_local).toBe( - community.counts.subscribers_local + 1, + expect(follow.community_view.community.subscribers_local).toBe( + community.community.subscribers_local + 1, ); // Test an unfollow let unfollow = await followCommunity(user, false, community.community.id); expect(unfollow.community_view.subscribed).toBe("NotSubscribed"); - expect(unfollow.community_view.counts.subscribers).toBe( - community.counts.subscribers, + expect(unfollow.community_view.community.subscribers).toBe( + community.community.subscribers, ); - expect(unfollow.community_view.counts.subscribers_local).toBe( - community.counts.subscribers_local, + expect(unfollow.community_view.community.subscribers_local).toBe( + community.community.subscribers_local, ); }); @@ -51,7 +51,7 @@ test("Follow federated community", async () => { const betaCommunityInitial = ( await waitUntil( () => resolveBetaCommunity(alpha), - c => !!c.community && c.community?.counts.subscribers >= 1, + c => !!c.community && c.community?.community.subscribers >= 1, ) ).community; if (!betaCommunityInitial) { @@ -74,14 +74,14 @@ test("Follow federated community", async () => { expect(betaCommunity?.community.local).toBe(false); expect(betaCommunity?.community.name).toBe("main"); expect(betaCommunity?.subscribed).toBe("Subscribed"); - expect(betaCommunity?.counts.subscribers_local).toBe( - betaCommunityInitial.counts.subscribers_local + 1, + expect(betaCommunity?.community.subscribers_local).toBe( + betaCommunityInitial.community.subscribers_local + 1, ); // check that unfollow was federated let communityOnBeta1 = await resolveBetaCommunity(beta); - expect(communityOnBeta1.community?.counts.subscribers).toBe( - betaCommunityInitial.counts.subscribers + 1, + expect(communityOnBeta1.community?.community.subscribers).toBe( + betaCommunityInitial.community.subscribers + 1, ); // Check it from local @@ -113,11 +113,11 @@ test("Follow federated community", async () => { let communityOnBeta2 = await waitUntil( () => resolveBetaCommunity(beta), c => - c.community?.counts.subscribers === - betaCommunityInitial.counts.subscribers, + c.community?.community.subscribers === + betaCommunityInitial.community.subscribers, ); - expect(communityOnBeta2.community?.counts.subscribers).toBe( - betaCommunityInitial.counts.subscribers, + expect(communityOnBeta2.community?.community.subscribers).toBe( + betaCommunityInitial.community.subscribers, ); - expect(communityOnBeta2.community?.counts.subscribers_local).toBe(1); + expect(communityOnBeta2.community?.community.subscribers_local).toBe(1); }); diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index b5cc72fe0..b1cf321d4 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -75,7 +75,7 @@ test("Upload image and delete it", async () => { expect(listAllMediaRes.images.length).toBe(previousThumbnails); // Make sure the uploader is correct - expect(listMediaRes.images[0].person.actor_id).toBe( + expect(listMediaRes.images[0].person.ap_id).toBe( `http://lemmy-alpha:8541/u/lemmy_alpha`, ); @@ -268,7 +268,7 @@ test("No image proxying if setting is disabled", async () => { let community = await createCommunity(alpha); let betaCommunity = await resolveCommunity( beta, - community.community_view.community.actor_id, + community.community_view.community.ap_id, ); await followCommunity(beta, true, betaCommunity.community!.community.id); diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 8ac46de22..09463238d 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -39,16 +39,20 @@ import { listReports, getMyUser, listInbox, + getModlog, } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; import { + AddModToCommunity, EditSite, + EditPost, PersonPostMentionView, PostReport, PostReportView, ReportCombinedView, ResolveObject, + ResolvePostReport, } from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -57,6 +61,11 @@ beforeAll(async () => { await setupLogins(); betaCommunity = (await resolveBetaCommunity(alpha)).community; expect(betaCommunity).toBeDefined(); + + // Hack: Force outgoing federation queue for beta to be created on epsilon, + // otherwise report test fails + let person = await resolvePerson(epsilon, "@lemmy_beta@lemmy-beta:8551"); + expect(person.person).toBeDefined(); }); afterAll(unfollows); @@ -89,7 +98,7 @@ async function assertPostFederation( expect(postOne?.post.embed_description).toBe(postTwo?.post.embed_description); expect(postOne?.post.embed_video_url).toBe(postTwo?.post.embed_video_url); expect(postOne?.post.published).toBe(postTwo?.post.published); - expect(postOne?.community.actor_id).toBe(postTwo?.community.actor_id); + expect(postOne?.community.ap_id).toBe(postTwo?.community.ap_id); expect(postOne?.post.locked).toBe(postTwo?.post.locked); expect(postOne?.post.removed).toBe(postTwo?.post.removed); expect(postOne?.post.deleted).toBe(postTwo?.post.deleted); @@ -116,19 +125,19 @@ test("Create a post", async () => { expect(postRes.post_view.post).toBeDefined(); expect(postRes.post_view.community.local).toBe(false); expect(postRes.post_view.creator.local).toBe(true); - expect(postRes.post_view.counts.score).toBe(1); + expect(postRes.post_view.post.score).toBe(1); // Make sure that post is liked on beta const betaPost = await waitForPost( beta, postRes.post_view.post, - res => res?.counts.score === 1, + res => res?.post.score === 1, ); expect(betaPost).toBeDefined(); expect(betaPost?.community.local).toBe(true); expect(betaPost?.creator.local).toBe(false); - expect(betaPost?.counts.score).toBe(1); + expect(betaPost?.post.score).toBe(1); await assertPostFederation(betaPost, postRes.post_view); // Delta only follows beta, so it should not see an alpha ap_id @@ -156,23 +165,23 @@ test("Unlike a post", async () => { } let postRes = await createPost(alpha, betaCommunity.community.id); let unlike = await likePost(alpha, 0, postRes.post_view.post); - expect(unlike.post_view.counts.score).toBe(0); + expect(unlike.post_view.post.score).toBe(0); // Try to unlike it again, make sure it stays at 0 let unlike2 = await likePost(alpha, 0, postRes.post_view.post); - expect(unlike2.post_view.counts.score).toBe(0); + expect(unlike2.post_view.post.score).toBe(0); // Make sure that post is unliked on beta const betaPost = await waitForPost( beta, postRes.post_view.post, - post => post?.counts.score === 0, + post => post?.post.score === 0, ); expect(betaPost).toBeDefined(); expect(betaPost?.community.local).toBe(true); expect(betaPost?.creator.local).toBe(false); - expect(betaPost?.counts.score).toBe(0); + expect(betaPost?.post.score).toBe(0); await assertPostFederation(betaPost, postRes.post_view); }); @@ -253,7 +262,7 @@ test("Collection of featured posts gets federated", async () => { // fetch the community, ensure that post is also fetched and marked as featured let betaCommunity = await resolveCommunity( beta, - community.community_view.community.actor_id, + community.community_view.community.ap_id, ); expect(betaCommunity).toBeDefined(); @@ -359,7 +368,7 @@ test("Remove a post from admin and community on different instance", async () => } let gammaCommunity = ( - await resolveCommunity(gamma, betaCommunity.community.actor_id) + await resolveCommunity(gamma, betaCommunity.community.ap_id) ).community?.community; if (!gammaCommunity) { throw "Missing gamma community"; @@ -398,7 +407,7 @@ test("Remove a post from admin and community on same instance", async () => { await followBeta(alpha); let gammaCommunity = await resolveCommunity( gamma, - betaCommunity.community.actor_id, + betaCommunity.community.ap_id, ); let postRes = await createPost(gamma, gammaCommunity.community!.community.id); expect(postRes.post_view.post).toBeDefined(); @@ -460,7 +469,7 @@ test("Enforce site ban federation for local user", async () => { // create a test user let alphaUserHttp = await registerUser(alpha, alphaUrl); let alphaUserPerson = (await getMyUser(alphaUserHttp)).local_user_view.person; - let alphaUserActorId = alphaUserPerson?.actor_id; + let alphaUserActorId = alphaUserPerson?.ap_id; if (!alphaUserActorId) { throw "Missing alpha user actor id"; } @@ -540,7 +549,7 @@ test("Enforce site ban federation for federated user", async () => { // create a test user let alphaUserHttp = await registerUser(alpha, alphaUrl); let alphaUserPerson = (await getMyUser(alphaUserHttp)).local_user_view.person; - let alphaUserActorId = alphaUserPerson?.actor_id; + let alphaUserActorId = alphaUserPerson?.ap_id; if (!alphaUserActorId) { throw "Missing alpha user actor id"; } @@ -644,14 +653,19 @@ test("Enforce community ban for federated user", async () => { ); expect(unBanAlpha.banned).toBe(false); - // Need to re-follow the community - await followBeta(alpha); + // Check that unban was federated to alpha + await waitUntil( + () => getModlog(alpha), + m => + m.modlog[0].type_ == "ModBanFromCommunity" && + m.modlog[0].mod_ban_from_community.banned == false, + ); let postRes3 = await createPost(alpha, betaCommunity.community.id); expect(postRes3.post_view.post).toBeDefined(); expect(postRes3.post_view.community.local).toBe(false); expect(postRes3.post_view.creator.local).toBe(true); - expect(postRes3.post_view.counts.score).toBe(1); + expect(postRes3.post_view.post.score).toBe(1); // Make sure that post makes it to beta community let postRes4 = await waitForPost(beta, postRes3.post_view.post); @@ -679,16 +693,26 @@ test("Report a post", async () => { // Create post from alpha let alphaCommunity = (await resolveBetaCommunity(alpha)).community!; await followBeta(alpha); - let postRes = await createPost(alpha, alphaCommunity.community.id); - expect(postRes.post_view.post).toBeDefined(); + let alphaPost = await createPost(alpha, alphaCommunity.community.id); + expect(alphaPost.post_view.post).toBeDefined(); - let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post; - if (!alphaPost) { - throw "Missing alpha post"; - } + // add remote mod on epsilon + await followBeta(epsilon); + + let betaCommunity = (await resolveBetaCommunity(beta)).community!; + let epsilonUser = ( + await resolvePerson(beta, "@lemmy_epsilon@lemmy-epsilon:8581") + ).person!; + let mod_params: AddModToCommunity = { + community_id: betaCommunity.community.id, + person_id: epsilonUser.person.id, + added: true, + }; + let res = await beta.addModToCommunity(mod_params); + expect(res.moderators.length).toBe(2); // Send report from gamma - let gammaPost = (await resolvePost(gamma, alphaPost.post)).post!; + let gammaPost = (await resolvePost(gamma, alphaPost.post_view.post)).post!; let gammaReport = ( await reportPost(gamma, gammaPost.post.id, randomString(10)) ).post_report_view.post_report; @@ -714,11 +738,12 @@ test("Report a post", async () => { expect(betaReport.reason).toBe(gammaReport.reason); await unfollowRemotes(alpha); - // Report was federated to poster's instance + // Report was federated to poster's instance. Alpha is not a community mod and doesnt see + // the report by default, so we need to pass show_mod_reports = true. let alphaReport = ( (await waitUntil( () => - listReports(alpha).then(p => + listReports(alpha, true).then(p => p.reports.find(r => { return checkPostReportName(r, gammaReport); }), @@ -732,6 +757,45 @@ test("Report a post", async () => { //expect(alphaReport.original_post_url).toBe(gammaReport.original_post_url); expect(alphaReport.original_post_body).toBe(gammaReport.original_post_body); expect(alphaReport.reason).toBe(gammaReport.reason); + + // Report was federated to remote mod instance + let epsilonReport = ( + (await waitUntil( + () => + listReports(epsilon).then(p => + p.reports.find(r => { + return checkPostReportName(r, gammaReport); + }), + ), + res => !!res, + ))! as PostReportView + ).post_report; + expect(epsilonReport).toBeDefined(); + expect(epsilonReport.resolved).toBe(false); + expect(epsilonReport.original_post_name).toBe(gammaReport.original_post_name); + + // Resolve report as remote mod + let resolve_params: ResolvePostReport = { + report_id: epsilonReport.id, + resolved: true, + }; + let resolve = await epsilon.resolvePostReport(resolve_params); + expect(resolve.post_report_view.post_report.resolved).toBeTruthy(); + + // Report should be marked resolved on community instance + let resolvedReport = ( + (await waitUntil( + () => + listReports(beta).then(p => + p.reports.find(r => { + return checkPostReportName(r, gammaReport) && r.resolver != null; + }), + ), + res => !!res, + ))! as PostReportView + ).post_report; + expect(resolvedReport).toBeDefined(); + expect(resolvedReport.resolved).toBe(true); }); test("Fetch post via redirect", async () => { @@ -742,7 +806,7 @@ test("Fetch post via redirect", async () => { const betaPost = await waitForPost( beta, alphaPost.post_view.post, - res => res?.counts.score === 1, + res => res?.post.score === 1, ); expect(betaPost).toBeDefined(); @@ -815,7 +879,7 @@ test("Mention beta from alpha post body", async () => { expect(postOnAlphaRes.post_view.post.body).toBeDefined(); expect(postOnAlphaRes.post_view.community.local).toBe(false); expect(postOnAlphaRes.post_view.creator.local).toBe(true); - expect(postOnAlphaRes.post_view.counts.score).toBe(1); + expect(postOnAlphaRes.post_view.post.score).toBe(1); // get beta's localized copy of the alpha post let betaPost = await waitForPost(beta, postOnAlphaRes.post_view.post); @@ -835,7 +899,7 @@ test("Mention beta from alpha post body", async () => { expect(firstMention.post.body).toBeDefined(); expect(firstMention.community.local).toBe(true); expect(firstMention.creator.local).toBe(false); - expect(firstMention.counts.score).toBe(1); + expect(firstMention.post.score).toBe(1); expect(firstMention.person_post_mention.post_id).toBe(betaPost.post.id); }); @@ -852,7 +916,6 @@ test("Rewrite markdown links", async () => { "https://example.com/", `[link](${postRes1.post_view.post.ap_id})`, ); - console.log(postRes2.post_view.post.body); expect(postRes2.post_view.post).toBeDefined(); // fetch both posts from another instance @@ -865,6 +928,50 @@ test("Rewrite markdown links", async () => { ); }); +test("Don't allow NSFW posts on instances that disable it", async () => { + // Disallow NSFW on gamma + let editSiteForm: EditSite = { + disallow_nsfw_content: true, + }; + await gamma.editSite(editSiteForm); + + // Wait for cache on Gamma's LocalSite + await delay(1_000); + + if (!betaCommunity) { + throw "Missing beta community"; + } + + // Make a NSFW post + let postRes = await createPost(beta, betaCommunity.community.id); + let form: EditPost = { + nsfw: true, + post_id: postRes.post_view.post.id, + }; + let updatePost = await beta.editPost(form); + + // Gamma reject resolving the post + await expect( + resolvePost(gamma, updatePost.post_view.post), + ).rejects.toStrictEqual(Error("not_found")); + + // Local users can't create NSFW post on Gamma + let gammaCommunity = ( + await resolveCommunity(gamma, betaCommunity.community.ap_id) + ).community?.community; + if (!gammaCommunity) { + throw "Missing gamma community"; + } + let gammaPost = await createPost(gamma, gammaCommunity.id); + let form2: EditPost = { + nsfw: true, + post_id: gammaPost.post_view.post.id, + }; + await expect(gamma.editPost(form2)).rejects.toStrictEqual( + Error("nsfw_not_allowed"), + ); +}); + function checkPostReportName(rcv: ReportCombinedView, report: PostReport) { switch (rcv.type_) { case "Post": diff --git a/api_tests/src/private_community.spec.ts b/api_tests/src/private_community.spec.ts index 65340a1dd..abe5336ad 100644 --- a/api_tests/src/private_community.spec.ts +++ b/api_tests/src/private_community.spec.ts @@ -47,7 +47,7 @@ test("Follow a private community", async () => { // follow as new user const user = await registerUser(beta, betaUrl); const betaCommunity = ( - await resolveCommunity(user, community.community_view.community.actor_id) + await resolveCommunity(user, community.community_view.community.ap_id) ).community; expect(betaCommunity).toBeDefined(); expect(betaCommunity?.community.visibility).toBe("Private"); @@ -134,7 +134,7 @@ test("Only followers can view and interact with private community content", asyn // user is not following the community and cannot view nor create posts const user = await registerUser(beta, betaUrl); const betaCommunity = ( - await resolveCommunity(user, community.community_view.community.actor_id) + await resolveCommunity(user, community.community_view.community.ap_id) ).community!.community; await expect(resolvePost(user, post0.post_view.post)).rejects.toStrictEqual( Error("not_found"), @@ -179,7 +179,7 @@ test("Reject follower", async () => { // user is not following the community and cannot view nor create posts const user = await registerUser(beta, betaUrl); const betaCommunity1 = ( - await resolveCommunity(user, community.community_view.community.actor_id) + await resolveCommunity(user, community.community_view.community.ap_id) ).community!.community; // follow the community and reject @@ -216,7 +216,7 @@ test("Follow a private community and receive activities", async () => { // follow with users from beta and gamma const betaCommunity = ( - await resolveCommunity(beta, community.community_view.community.actor_id) + await resolveCommunity(beta, community.community_view.community.ap_id) ).community; expect(betaCommunity).toBeDefined(); const betaCommunityId = betaCommunity!.community.id; @@ -228,7 +228,7 @@ test("Follow a private community and receive activities", async () => { await approveFollower(alpha, alphaCommunityId); const gammaCommunityId = ( - await resolveCommunity(gamma, community.community_view.community.actor_id) + await resolveCommunity(gamma, community.community_view.community.ap_id) ).community!.community.id; const follow_form_gamma: FollowCommunity = { community_id: gammaCommunityId, @@ -281,7 +281,7 @@ test("Fetch remote content in private community", async () => { const alphaCommunityId = community.community_view.community.id; const betaCommunityId = ( - await resolveCommunity(beta, community.community_view.community.actor_id) + await resolveCommunity(beta, community.community_view.community.ap_id) ).community!.community.id; const follow_form_beta: FollowCommunity = { community_id: betaCommunityId, @@ -312,7 +312,7 @@ test("Fetch remote content in private community", async () => { // create gamma user const gammaCommunityId = ( - await resolveCommunity(gamma, community.community_view.community.actor_id) + await resolveCommunity(gamma, community.community_view.community.ap_id) ).community!.community.id; const follow_form: FollowCommunity = { community_id: gammaCommunityId, diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 5e9513df8..e7d86c289 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -27,6 +27,8 @@ import { ListInboxResponse, ListInbox, InboxDataType, + GetModlogResponse, + GetModlog, } from "lemmy-js-client"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { DeletePost } from "lemmy-js-client/dist/types/DeletePost"; @@ -83,6 +85,7 @@ import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDetailsResponse"; import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails"; import { ListingType } from "lemmy-js-client/dist/types/ListingType"; +import { GetCommunityPendingFollowsCountI } from "lemmy-js-client/dist/other_types"; export const fetchFunction = fetch; export const imageFetchLimit = 50; @@ -199,7 +202,7 @@ export async function setupLogins() { } } -async function allowInstance(api: LemmyHttp, instance: string) { +export async function allowInstance(api: LemmyHttp, instance: string) { const params: AdminAllowInstanceParams = { instance, allow: true, @@ -324,9 +327,8 @@ export async function searchPostLocal( post: Post, ): Promise { let form: Search = { - q: post.name, + search_term: post.name, type_: "Posts", - sort: "TopAll", listing_type: "All", }; return api.search(form); @@ -339,7 +341,7 @@ export async function waitForPost( checker: (t: PostView | undefined) => boolean = p => !!p, ) { return waitUntil( - () => searchPostLocal(api, post).then(p => p.posts[0]), + () => searchPostLocal(api, post).then(p => p.results[0] as PostView), checker, ); } @@ -801,8 +803,9 @@ export async function reportPost( export async function listReports( api: LemmyHttp, + show_community_rule_violations: boolean = false, ): Promise { - let form: ListReports = {}; + let form: ListReports = { show_community_rule_violations }; return api.listReports(form); } @@ -883,7 +886,8 @@ export function getCommunityPendingFollowsCount( api: LemmyHttp, community_id: CommunityId, ): Promise { - return api.getCommunityPendingFollowsCount(community_id); + let form: GetCommunityPendingFollowsCountI = { community_id }; + return api.getCommunityPendingFollowsCount(form); } export function approveCommunityPendingFollow( @@ -899,6 +903,10 @@ export function approveCommunityPendingFollow( }; return api.approveCommunityPendingFollow(form); } +export function getModlog(api: LemmyHttp): Promise { + let form: GetModlog = {}; + return api.getModlog(form); +} export function delay(millis = 500) { return new Promise(resolve => setTimeout(resolve, millis)); diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index edb8a4c83..f4e5e270c 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -41,7 +41,7 @@ function assertUserFederation(userOne?: PersonView, userTwo?: PersonView) { expect(userOne?.person.name).toBe(userTwo?.person.name); expect(userOne?.person.display_name).toBe(userTwo?.person.display_name); expect(userOne?.person.bio).toBe(userTwo?.person.bio); - expect(userOne?.person.actor_id).toBe(userTwo?.person.actor_id); + expect(userOne?.person.ap_id).toBe(userTwo?.person.ap_id); expect(userOne?.person.avatar).toBe(userTwo?.person.avatar); expect(userOne?.person.banner).toBe(userTwo?.person.banner); expect(userOne?.person.published).toBe(userTwo?.person.published); @@ -181,7 +181,7 @@ test("Create user with accept-language", async () => { .map(l => l.code); // should have languages from accept header, as well as "undetermined" // which is automatically enabled by backend - expect(langs).toStrictEqual(["und", "de", "en", "fr"]); + expect(langs).toStrictEqual(["de", "en", "fr"]); }); test("Set a new avatar, old avatar is deleted", async () => { diff --git a/config/defaults.hjson b/config/defaults.hjson index b5d3b1004..7b34f38b0 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -59,27 +59,25 @@ upload_timeout: 30 # Resize post thumbnails to this maximum width/height. max_thumbnail_size: 512 - # Maximum size for user avatar, community icon and site icon. + # Maximum size for user avatar, community icon and site icon. Larger images are downscaled. max_avatar_size: 512 - # Maximum size for user, community and site banner. Larger images are downscaled to fit - # into a square of this size. + # Maximum size for user, community and site banner. Larger images are downscaled. max_banner_size: 1024 + # Maximum size for other uploads (e.g. post images or markdown embed images). Larger + # images are downscaled. + max_upload_size: 1024 + # Whether users can upload videos as post image or markdown embed. + allow_video_uploads: true # Prevent users from uploading images for posts or embedding in markdown. Avatars, icons and # banners can still be uploaded. image_upload_disabled: false } # Email sending configuration. All options except login/password are mandatory email: { - # Hostname and port of the smtp server - smtp_server: "localhost:25" - # Login name for smtp server - smtp_login: "string" - # Password to login to the smtp server - smtp_password: "string" + # https://docs.rs/lettre/0.11.14/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url + connection: "smtps://user:pass@hostname:port" # Address to send emails from, eg "noreply@your-instance.com" smtp_from_address: "noreply@example.com" - # Whether or not smtp connections should use tls. Can be none, tls, or starttls - tls_type: "none" } # Parameters for automatic configuration of new instance (only used at first start) setup: { @@ -110,7 +108,13 @@ bind: "127.0.0.1" port: 10002 } - # Sets a response Access-Control-Allow-Origin CORS header + # Sets a response Access-Control-Allow-Origin CORS header. Can also be set via environment: + # `LEMMY_CORS_ORIGIN=example.org,site.com` # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - cors_origin: "lemmy.tld" + cors_origin: [ + "lemmy.tld" + /* ... */ + ] + # Print logs in JSON format. You can also disable ANSI colors in logs with env var `NO_COLOR`. + json_logging: false } diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 077426d32..7d954d968 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -21,8 +21,6 @@ workspace = true lemmy_utils = { workspace = true } lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] } -lemmy_db_views_moderator = { workspace = true, features = ["full"] } -lemmy_db_views_actor = { workspace = true, features = ["full"] } lemmy_api_common = { workspace = true, features = ["full"] } activitypub_federation = { workspace = true } bcrypt = { workspace = true } @@ -33,10 +31,10 @@ anyhow = { workspace = true } tracing = { workspace = true } chrono = { workspace = true } url = { workspace = true } +regex = { workspace = true } hound = "3.5.1" sitemap-rs = "0.2.2" totp-rs = { version = "5.6.0", features = ["gen_secret", "otpauth"] } -actix-web-httpauth = "0.8.2" [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/api/src/comment/distinguish.rs b/crates/api/src/comment/distinguish.rs index 17608a230..35c53b5d1 100644 --- a/crates/api/src/comment/distinguish.rs +++ b/crates/api/src/comment/distinguish.rs @@ -11,7 +11,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn distinguish_comment( data: Json, context: Data, diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index 0815b3863..0b0151203 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -20,7 +20,6 @@ use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use std::ops::Deref; -#[tracing::instrument(skip(context))] pub async fn like_comment( data: Json, context: Data, diff --git a/crates/api/src/comment/list_comment_likes.rs b/crates/api/src/comment/list_comment_likes.rs index c9721b8a0..2ee7e7345 100644 --- a/crates/api/src/comment/list_comment_likes.rs +++ b/crates/api/src/comment/list_comment_likes.rs @@ -8,7 +8,6 @@ use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView}; use lemmy_utils::error::LemmyResult; /// Lists likes for a comment -#[tracing::instrument(skip(context))] pub async fn list_comment_likes( data: Query, context: Data, diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs index cca6d06bc..46f9d8abe 100644 --- a/crates/api/src/comment/save.rs +++ b/crates/api/src/comment/save.rs @@ -10,7 +10,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn save_comment( data: Json, context: Data, diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 4c5b4eae5..6c221e39d 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -14,11 +14,9 @@ use lemmy_db_schema::{ }, traits::{Crud, Joinable}, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityModeratorView; +use lemmy_db_views::structs::{CommunityModeratorView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn add_mod_to_community( data: Json, context: Data, diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 547838fa7..83e2e05e7 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -24,14 +24,12 @@ use lemmy_db_schema::{ }, traits::{Bannable, Crud, Followable}, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PersonView; +use lemmy_db_views::structs::{LocalUserView, PersonView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::validation::is_valid_body_field, }; -#[tracing::instrument(skip(context))] pub async fn ban_from_community( data: Json, context: Data, diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs index d49872493..dbed2afc5 100644 --- a/crates/api/src/community/block.rs +++ b/crates/api/src/community/block.rs @@ -12,11 +12,9 @@ use lemmy_db_schema::{ }, traits::{Blockable, Followable}, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityView; +use lemmy_db_views::structs::{CommunityView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn user_block_community( data: Json, context: Data, diff --git a/crates/api/src/community/follow.rs b/crates/api/src/community/follow.rs index d5cd3e5b1..f0318ec23 100644 --- a/crates/api/src/community/follow.rs +++ b/crates/api/src/community/follow.rs @@ -14,11 +14,9 @@ use lemmy_db_schema::{ traits::{Crud, Followable}, CommunityVisibility, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView}; +use lemmy_db_views::structs::{CommunityPersonBanView, CommunityView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn follow_community( data: Json, context: Data, diff --git a/crates/api/src/community/hide.rs b/crates/api/src/community/hide.rs index f494ad732..446ac8c96 100644 --- a/crates/api/src/community/hide.rs +++ b/crates/api/src/community/hide.rs @@ -17,7 +17,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn hide_community( data: Json, context: Data, diff --git a/crates/api/src/community/pending_follows/count.rs b/crates/api/src/community/pending_follows/count.rs index e8e333c84..885ce0585 100644 --- a/crates/api/src/community/pending_follows/count.rs +++ b/crates/api/src/community/pending_follows/count.rs @@ -4,8 +4,7 @@ use lemmy_api_common::{ context::LemmyContext, utils::is_mod_or_admin, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_db_views::structs::{CommunityFollowerView, LocalUserView}; use lemmy_utils::error::LemmyResult; pub async fn get_pending_follows_count( diff --git a/crates/api/src/community/pending_follows/list.rs b/crates/api/src/community/pending_follows/list.rs index 9f300a74f..bf56478f0 100644 --- a/crates/api/src/community/pending_follows/list.rs +++ b/crates/api/src/community/pending_follows/list.rs @@ -4,8 +4,7 @@ use lemmy_api_common::{ context::LemmyContext, utils::check_community_mod_of_any_or_admin_action, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_db_views::structs::{CommunityFollowerView, LocalUserView}; use lemmy_utils::error::LemmyResult; pub async fn get_pending_follows_list( diff --git a/crates/api/src/community/random.rs b/crates/api/src/community/random.rs index 55941229d..2f4110616 100644 --- a/crates/api/src/community/random.rs +++ b/crates/api/src/community/random.rs @@ -10,11 +10,9 @@ use lemmy_db_schema::source::{ community::Community, local_site::LocalSite, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityView; +use lemmy_db_views::structs::{CommunityView, LocalUserView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn get_random_community( data: Query, context: Data, diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index e60b50aa2..9453aced2 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -12,8 +12,7 @@ use lemmy_db_schema::{ }, traits::{Crud, Joinable}, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; +use lemmy_db_views::structs::{CommunityModeratorView, CommunityView, LocalUserView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, location_info, @@ -21,7 +20,7 @@ use lemmy_utils::{ // TODO: we dont do anything for federation here, it should be updated the next time the community // gets fetched. i hope we can get rid of the community creator role soon. -#[tracing::instrument(skip(context))] + pub async fn transfer_community( data: Json, context: Data, diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index aa6e37000..fe0c91abd 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,14 +1,11 @@ use activitypub_federation::config::Data; -use actix_web::{http::header::Header, HttpRequest}; -use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine}; use captcha::Captcha; use lemmy_api_common::{ - claims::Claims, community::BanFromCommunity, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_expire_time, check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME}, + utils::check_expire_time, }; use lemmy_db_schema::{ source::{ @@ -18,7 +15,6 @@ use lemmy_db_schema::{ CommunityPersonBan, CommunityPersonBanForm, }, - local_site::LocalSite, mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, person::Person, }, @@ -26,9 +22,10 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ - error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult}, + error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::slurs::check_slurs, }; +use regex::Regex; use std::io::Cursor; use totp_rs::{Secret, TOTP}; @@ -82,9 +79,7 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult { } /// Check size of report -pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> LemmyResult<()> { - let slur_regex = &local_site_to_slur_regex(local_site); - +pub(crate) fn check_report_reason(reason: &str, slur_regex: &Regex) -> LemmyResult<()> { check_slurs(reason, slur_regex)?; if reason.is_empty() { Err(LemmyErrorType::ReportReasonRequired)? @@ -95,21 +90,6 @@ pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Lemmy } } -pub fn read_auth_token(req: &HttpRequest) -> LemmyResult> { - // Try reading jwt from auth header - if let Ok(header) = Authorization::::parse(req) { - Ok(Some(header.as_ref().token().to_string())) - } - // If that fails, try to read from cookie - else if let Some(cookie) = &req.cookie(AUTH_COOKIE_NAME) { - Ok(Some(cookie.value().to_string())) - } - // Otherwise, there's no auth - else { - Ok(None) - } -} - pub(crate) fn check_totp_2fa_valid( local_user_view: &LocalUserView, totp_token: &Option, @@ -164,7 +144,6 @@ fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> LemmyResult LemmyResult { - let local_user_id = Claims::validate(jwt, context) - .await - .with_lemmy_type(LemmyErrorType::NotLoggedIn)?; - let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; - check_user_valid(&local_user_view.person)?; - - Ok(local_user_view) -} - #[cfg(test)] mod tests { diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs index 1e821bf3e..1199dce3b 100644 --- a/crates/api/src/local_user/add_admin.rs +++ b/crates/api/src/local_user/add_admin.rs @@ -11,11 +11,9 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PersonView; +use lemmy_db_views::structs::{LocalUserView, PersonView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn add_admin( data: Json, context: Data, diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 715bd206d..8ff34975c 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -16,14 +16,12 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PersonView; +use lemmy_db_views::structs::{LocalUserView, PersonView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::validation::is_valid_body_field, }; -#[tracing::instrument(skip(context))] pub async fn ban_from_site( data: Json, context: Data, diff --git a/crates/api/src/local_user/block.rs b/crates/api/src/local_user/block.rs index 3aee554d4..34798ce4d 100644 --- a/crates/api/src/local_user/block.rs +++ b/crates/api/src/local_user/block.rs @@ -7,11 +7,9 @@ use lemmy_db_schema::{ source::person_block::{PersonBlock, PersonBlockForm}, traits::Blockable, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PersonView; +use lemmy_db_views::structs::{LocalUserView, PersonView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn user_block_person( data: Json, context: Data, diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs index 03f873a0f..a864ad150 100644 --- a/crates/api/src/local_user/change_password.rs +++ b/crates/api/src/local_user/change_password.rs @@ -13,7 +13,6 @@ use lemmy_db_schema::source::{local_user::LocalUser, login_token::LoginToken}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn change_password( data: Json, req: HttpRequest, diff --git a/crates/api/src/local_user/change_password_after_reset.rs b/crates/api/src/local_user/change_password_after_reset.rs index df99952f4..5524da33b 100644 --- a/crates/api/src/local_user/change_password_after_reset.rs +++ b/crates/api/src/local_user/change_password_after_reset.rs @@ -12,7 +12,6 @@ use lemmy_db_schema::source::{ }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn change_password_after_reset( data: Json, context: Data, diff --git a/crates/api/src/local_user/generate_totp_secret.rs b/crates/api/src/local_user/generate_totp_secret.rs index 03ba69759..7e60c4384 100644 --- a/crates/api/src/local_user/generate_totp_secret.rs +++ b/crates/api/src/local_user/generate_totp_secret.rs @@ -11,7 +11,6 @@ use lemmy_utils::error::{LemmyErrorType, LemmyResult}; /// Generate a new secret for two-factor-authentication. Afterwards you need to call [toggle_totp] /// to enable it. This can only be called if 2FA is currently disabled. -#[tracing::instrument(skip(context))] pub async fn generate_totp_secret( local_user_view: LocalUserView, context: Data, diff --git a/crates/api/src/local_user/get_captcha.rs b/crates/api/src/local_user/get_captcha.rs index 485b95009..a4f603c68 100644 --- a/crates/api/src/local_user/get_captcha.rs +++ b/crates/api/src/local_user/get_captcha.rs @@ -20,7 +20,6 @@ use lemmy_db_schema::source::{ }; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn get_captcha(context: Data) -> LemmyResult { let local_site = LocalSite::read(&mut context.pool()).await?; let mut res = HttpResponseBuilder::new(StatusCode::OK); diff --git a/crates/api/src/local_user/list_banned.rs b/crates/api/src/local_user/list_banned.rs index ba2c0d403..d4227ca37 100644 --- a/crates/api/src/local_user/list_banned.rs +++ b/crates/api/src/local_user/list_banned.rs @@ -1,7 +1,6 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{context::LemmyContext, person::BannedPersonsResponse, utils::is_admin}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PersonView; +use lemmy_db_views::structs::{LocalUserView, PersonView}; use lemmy_utils::error::LemmyResult; pub async fn list_banned_users( diff --git a/crates/api/src/local_user/list_media.rs b/crates/api/src/local_user/list_media.rs index 779558dab..d907740ef 100644 --- a/crates/api/src/local_user/list_media.rs +++ b/crates/api/src/local_user/list_media.rs @@ -6,7 +6,6 @@ use lemmy_api_common::{ use lemmy_db_views::structs::{LocalImageView, LocalUserView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn list_media( data: Query, context: Data, diff --git a/crates/api/src/local_user/list_saved.rs b/crates/api/src/local_user/list_saved.rs index bdb1b6b0a..32fc3da62 100644 --- a/crates/api/src/local_user/list_saved.rs +++ b/crates/api/src/local_user/list_saved.rs @@ -5,13 +5,13 @@ use lemmy_api_common::{ person::{ListPersonSaved, ListPersonSavedResponse}, utils::check_private_instance, }; +use lemmy_db_schema::traits::PaginationCursorBuilder; use lemmy_db_views::{ - person_saved_combined_view::PersonSavedCombinedQuery, - structs::{LocalUserView, SiteView}, + combined::person_saved_combined_view::PersonSavedCombinedQuery, + structs::{LocalUserView, PersonSavedCombinedView, SiteView}, }; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn list_person_saved( data: Query, context: Data, @@ -21,22 +21,21 @@ pub async fn list_person_saved( check_private_instance(&Some(local_user_view.clone()), &local_site.local_site)?; - // parse pagination token - let page_after = if let Some(pa) = &data.page_cursor { - Some(pa.read(&mut context.pool()).await?) + let cursor_data = if let Some(cursor) = &data.page_cursor { + Some(PersonSavedCombinedView::from_cursor(cursor, &mut context.pool()).await?) } else { None }; - let page_back = data.page_back; - let type_ = data.type_; let saved = PersonSavedCombinedQuery { - type_, - page_after, - page_back, + type_: data.type_, + cursor_data, + page_back: data.page_back, } .list(&mut context.pool(), &local_user_view) .await?; - Ok(Json(ListPersonSavedResponse { saved })) + let next_page = saved.last().map(PaginationCursorBuilder::to_cursor); + + Ok(Json(ListPersonSavedResponse { saved, next_page })) } diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index 0b2514c5b..abc04eb29 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -13,7 +13,6 @@ use lemmy_api_common::{ use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn login( data: Json, req: HttpRequest, diff --git a/crates/api/src/local_user/logout.rs b/crates/api/src/local_user/logout.rs index 10b4732b7..5fe155c44 100644 --- a/crates/api/src/local_user/logout.rs +++ b/crates/api/src/local_user/logout.rs @@ -1,12 +1,14 @@ -use crate::read_auth_token; use activitypub_federation::config::Data; use actix_web::{cookie::Cookie, HttpRequest, HttpResponse}; -use lemmy_api_common::{context::LemmyContext, utils::AUTH_COOKIE_NAME, SuccessResponse}; +use lemmy_api_common::{ + context::LemmyContext, + utils::{read_auth_token, AUTH_COOKIE_NAME}, + SuccessResponse, +}; use lemmy_db_schema::source::login_token::LoginToken; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn logout( req: HttpRequest, // require login diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index 15243056d..fc2da0b2f 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -14,6 +14,7 @@ pub mod login; pub mod logout; pub mod notifications; pub mod report_count; +pub mod resend_verification_email; pub mod reset_password; pub mod save_settings; pub mod update_totp; diff --git a/crates/api/src/local_user/notifications/list_inbox.rs b/crates/api/src/local_user/notifications/list_inbox.rs index 7d6e88468..f5224210f 100644 --- a/crates/api/src/local_user/notifications/list_inbox.rs +++ b/crates/api/src/local_user/notifications/list_inbox.rs @@ -3,38 +3,37 @@ use lemmy_api_common::{ context::LemmyContext, person::{ListInbox, ListInboxResponse}, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::inbox_combined_view::InboxCombinedQuery; +use lemmy_db_schema::traits::PaginationCursorBuilder; +use lemmy_db_views::{ + combined::inbox_combined_view::InboxCombinedQuery, + structs::{InboxCombinedView, LocalUserView}, +}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn list_inbox( data: Query, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let unread_only = data.unread_only; - let type_ = data.type_; let person_id = local_user_view.person.id; - let show_bot_accounts = Some(local_user_view.local_user.show_bot_accounts); - // parse pagination token - let page_after = if let Some(pa) = &data.page_cursor { - Some(pa.read(&mut context.pool()).await?) + let cursor_data = if let Some(cursor) = &data.page_cursor { + Some(InboxCombinedView::from_cursor(cursor, &mut context.pool()).await?) } else { None }; - let page_back = data.page_back; let inbox = InboxCombinedQuery { - type_, - unread_only, - show_bot_accounts, - page_after, - page_back, + type_: data.type_, + unread_only: data.unread_only, + show_bot_accounts: Some(local_user_view.local_user.show_bot_accounts), + cursor_data, + page_back: data.page_back, } .list(&mut context.pool(), person_id) .await?; - Ok(Json(ListInboxResponse { inbox })) + let next_page = inbox.last().map(PaginationCursorBuilder::to_cursor); + + Ok(Json(ListInboxResponse { inbox, next_page })) } diff --git a/crates/api/src/local_user/notifications/mark_all_read.rs b/crates/api/src/local_user/notifications/mark_all_read.rs index 9ba0916f8..fc9f5a32d 100644 --- a/crates/api/src/local_user/notifications/mark_all_read.rs +++ b/crates/api/src/local_user/notifications/mark_all_read.rs @@ -9,7 +9,6 @@ use lemmy_db_schema::source::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn mark_all_notifications_read( context: Data, local_user_view: LocalUserView, diff --git a/crates/api/src/local_user/notifications/mark_comment_mention_read.rs b/crates/api/src/local_user/notifications/mark_comment_mention_read.rs index e7091549e..5f076283a 100644 --- a/crates/api/src/local_user/notifications/mark_comment_mention_read.rs +++ b/crates/api/src/local_user/notifications/mark_comment_mention_read.rs @@ -11,7 +11,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn mark_comment_mention_as_read( data: Json, context: Data, diff --git a/crates/api/src/local_user/notifications/mark_post_mention_read.rs b/crates/api/src/local_user/notifications/mark_post_mention_read.rs index 954435cb7..5e502f004 100644 --- a/crates/api/src/local_user/notifications/mark_post_mention_read.rs +++ b/crates/api/src/local_user/notifications/mark_post_mention_read.rs @@ -11,7 +11,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn mark_post_mention_as_read( data: Json, context: Data, diff --git a/crates/api/src/local_user/notifications/mark_reply_read.rs b/crates/api/src/local_user/notifications/mark_reply_read.rs index 4a1017ce1..bf39025d5 100644 --- a/crates/api/src/local_user/notifications/mark_reply_read.rs +++ b/crates/api/src/local_user/notifications/mark_reply_read.rs @@ -7,7 +7,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn mark_reply_as_read( data: Json, context: Data, diff --git a/crates/api/src/local_user/notifications/unread_count.rs b/crates/api/src/local_user/notifications/unread_count.rs index 4fa959329..9c03697b8 100644 --- a/crates/api/src/local_user/notifications/unread_count.rs +++ b/crates/api/src/local_user/notifications/unread_count.rs @@ -1,10 +1,8 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::InboxCombinedViewInternal; +use lemmy_db_views::structs::{InboxCombinedViewInternal, LocalUserView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn unread_count( context: Data, local_user_view: LocalUserView, diff --git a/crates/api/src/local_user/report_count.rs b/crates/api/src/local_user/report_count.rs index 0d24a4de9..7fdb5c421 100644 --- a/crates/api/src/local_user/report_count.rs +++ b/crates/api/src/local_user/report_count.rs @@ -7,7 +7,6 @@ use lemmy_api_common::{ use lemmy_db_views::structs::{LocalUserView, ReportCombinedViewInternal}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn report_count( data: Query, context: Data, diff --git a/crates/api/src/local_user/resend_verification_email.rs b/crates/api/src/local_user/resend_verification_email.rs new file mode 100644 index 000000000..72122ef61 --- /dev/null +++ b/crates/api/src/local_user/resend_verification_email.rs @@ -0,0 +1,30 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{ + context::LemmyContext, + person::ResendVerificationEmail, + utils::send_verification_email_if_required, + SuccessResponse, +}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; +use lemmy_utils::error::LemmyResult; + +pub async fn resend_verification_email( + data: Json, + context: Data, +) -> LemmyResult> { + let site_view = SiteView::read_local(&mut context.pool()).await?; + let email = data.email.to_string(); + + // Fetch that email + let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await?; + + send_verification_email_if_required( + &context, + &site_view.local_site, + &local_user_view.local_user, + &local_user_view.person, + ) + .await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs index 20707950c..032c20be7 100644 --- a/crates/api/src/local_user/reset_password.rs +++ b/crates/api/src/local_user/reset_password.rs @@ -9,7 +9,6 @@ use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::error::LemmyResult; use tracing::error; -#[tracing::instrument(skip(context))] pub async fn reset_password( data: Json, context: Data, diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index 6c2f6e1d1..dab9dfb9d 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -3,23 +3,17 @@ use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, person::SaveUserSettings, - utils::{ - get_url_blocklist, - local_site_to_slur_regex, - process_markdown_opt, - send_verification_email, - }, + utils::{get_url_blocklist, process_markdown_opt, send_verification_email, slur_regex}, SuccessResponse, }; use lemmy_db_schema::{ source::{ actor_language::LocalUserLanguage, local_user::{LocalUser, LocalUserUpdateForm}, - local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeUpdateForm}, person::{Person, PersonUpdateForm}, }, traits::Crud, - utils::diesel_string_update, + utils::{diesel_opt_number_update, diesel_string_update}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ @@ -28,7 +22,6 @@ use lemmy_utils::{ }; use std::ops::Deref; -#[tracing::instrument(skip(context))] pub async fn save_user_settings( data: Json, context: Data, @@ -36,7 +29,7 @@ pub async fn save_user_settings( ) -> LemmyResult> { let site_view = SiteView::read_local(&mut context.pool()).await?; - let slur_regex = local_site_to_slur_regex(&site_view.local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let bio = diesel_string_update( process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context) @@ -55,7 +48,9 @@ pub async fn save_user_settings( if previous_email.deref() != email { LocalUser::check_is_email_taken(&mut context.pool(), email).await?; send_verification_email( - &local_user_view, + &site_view.local_site, + &local_user_view.local_user, + &local_user_view.person, email, &mut context.pool(), context.settings(), @@ -91,6 +86,8 @@ pub async fn save_user_settings( let person_id = local_user_view.person.id; let default_listing_type = data.default_listing_type; let default_post_sort_type = data.default_post_sort_type; + let default_post_time_range_seconds = + diesel_opt_number_update(data.default_post_time_range_seconds); let default_comment_sort_type = data.default_comment_sort_type; let person_form = PersonUpdateForm { @@ -120,6 +117,7 @@ pub async fn save_user_settings( blur_nsfw: data.blur_nsfw, show_bot_accounts: data.show_bot_accounts, default_post_sort_type, + default_post_time_range_seconds, default_comment_sort_type, default_listing_type, theme: data.theme.clone(), @@ -133,20 +131,15 @@ pub async fn save_user_settings( collapse_bot_comments: data.collapse_bot_comments, auto_mark_fetched_posts_as_read: data.auto_mark_fetched_posts_as_read, hide_media: data.hide_media, + // Update the vote display modes + show_score: data.show_scores, + show_upvotes: data.show_upvotes, + show_downvotes: data.show_downvotes, + show_upvote_percentage: data.show_upvote_percentage, ..Default::default() }; LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?; - // Update the vote display modes - let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm { - score: data.show_scores, - upvotes: data.show_upvotes, - downvotes: data.show_downvotes, - upvote_percentage: data.show_upvote_percentage, - }; - LocalUserVoteDisplayMode::update(&mut context.pool(), local_user_id, &vote_display_modes_form) - .await?; - Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/local_user/update_totp.rs b/crates/api/src/local_user/update_totp.rs index c28ac7228..5d728ba02 100644 --- a/crates/api/src/local_user/update_totp.rs +++ b/crates/api/src/local_user/update_totp.rs @@ -16,7 +16,6 @@ use lemmy_utils::error::LemmyResult; /// /// Disabling is only possible if 2FA was previously enabled. Again it is necessary to pass a valid /// token. -#[tracing::instrument(skip(context))] pub async fn update_totp( data: Json, local_user_view: LocalUserView, diff --git a/crates/api/src/local_user/user_block_instance.rs b/crates/api/src/local_user/user_block_instance.rs index 940538833..e65f5f5ef 100644 --- a/crates/api/src/local_user/user_block_instance.rs +++ b/crates/api/src/local_user/user_block_instance.rs @@ -8,7 +8,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn user_block_instance( data: Json, local_user_view: LocalUserView, diff --git a/crates/api/src/local_user/validate_auth.rs b/crates/api/src/local_user/validate_auth.rs index 36d31ff01..0467c5d9b 100644 --- a/crates/api/src/local_user/validate_auth.rs +++ b/crates/api/src/local_user/validate_auth.rs @@ -1,14 +1,16 @@ -use crate::{local_user_view_from_jwt, read_auth_token}; use actix_web::{ web::{Data, Json}, HttpRequest, }; -use lemmy_api_common::{context::LemmyContext, SuccessResponse}; +use lemmy_api_common::{ + context::LemmyContext, + utils::{local_user_view_from_jwt, read_auth_token}, + SuccessResponse, +}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; /// Returns an error message if the auth token is invalid for any reason. Necessary because other /// endpoints silently treat any call with invalid auth as unauthenticated. -#[tracing::instrument(skip(context))] pub async fn validate_auth( req: HttpRequest, context: Data, diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index 7f2415e38..bfd6690eb 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -19,7 +19,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn feature_post( data: Json, context: Data, diff --git a/crates/api/src/post/get_link_metadata.rs b/crates/api/src/post/get_link_metadata.rs index a777cab17..add05ce1c 100644 --- a/crates/api/src/post/get_link_metadata.rs +++ b/crates/api/src/post/get_link_metadata.rs @@ -8,7 +8,6 @@ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use url::Url; -#[tracing::instrument(skip(context))] pub async fn get_link_metadata( data: Query, context: Data, @@ -16,7 +15,7 @@ pub async fn get_link_metadata( _local_user_view: LocalUserView, ) -> LemmyResult> { let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?; - let metadata = fetch_link_metadata(&url, &context).await?; + let metadata = fetch_link_metadata(&url, &context, false).await?; Ok(Json(GetSiteMetadataResponse { metadata })) } diff --git a/crates/api/src/post/hide.rs b/crates/api/src/post/hide.rs index 58464421c..969577a99 100644 --- a/crates/api/src/post/hide.rs +++ b/crates/api/src/post/hide.rs @@ -7,7 +7,6 @@ use lemmy_db_schema::source::post::PostHide; use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn hide_post( data: Json, context: Data, diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index 6555228e9..3f552abf1 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -19,7 +19,6 @@ use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use std::ops::Deref; -#[tracing::instrument(skip(context))] pub async fn like_post( data: Json, context: Data, diff --git a/crates/api/src/post/list_post_likes.rs b/crates/api/src/post/list_post_likes.rs index a9b302f2e..b200fb0a6 100644 --- a/crates/api/src/post/list_post_likes.rs +++ b/crates/api/src/post/list_post_likes.rs @@ -9,7 +9,6 @@ use lemmy_db_views::structs::{LocalUserView, VoteView}; use lemmy_utils::error::LemmyResult; /// Lists likes for a post -#[tracing::instrument(skip(context))] pub async fn list_post_likes( data: Query, context: Data, diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index ad7fa7264..dd2025235 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -17,7 +17,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn lock_post( data: Json, context: Data, diff --git a/crates/api/src/post/mark_many_read.rs b/crates/api/src/post/mark_many_read.rs index 82c2c0b06..9a7330d2f 100644 --- a/crates/api/src/post/mark_many_read.rs +++ b/crates/api/src/post/mark_many_read.rs @@ -4,7 +4,6 @@ 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, diff --git a/crates/api/src/post/mark_read.rs b/crates/api/src/post/mark_read.rs index 2d3284375..be9b30798 100644 --- a/crates/api/src/post/mark_read.rs +++ b/crates/api/src/post/mark_read.rs @@ -7,7 +7,6 @@ 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, diff --git a/crates/api/src/post/save.rs b/crates/api/src/post/save.rs index cebbd7fd5..54a423ae3 100644 --- a/crates/api/src/post/save.rs +++ b/crates/api/src/post/save.rs @@ -10,7 +10,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn save_post( data: Json, context: Data, diff --git a/crates/api/src/private_message/mark_read.rs b/crates/api/src/private_message/mark_read.rs index 128228d6d..d989466bd 100644 --- a/crates/api/src/private_message/mark_read.rs +++ b/crates/api/src/private_message/mark_read.rs @@ -11,7 +11,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn mark_pm_as_read( data: Json, context: Data, diff --git a/crates/api/src/reports/comment_report/create.rs b/crates/api/src/reports/comment_report/create.rs index a456ded36..6187aac30 100644 --- a/crates/api/src/reports/comment_report/create.rs +++ b/crates/api/src/reports/comment_report/create.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ check_comment_deleted_or_removed, check_community_user_action, send_new_report_email_to_admins, + slur_regex, }, }; use lemmy_db_schema::{ @@ -22,16 +23,14 @@ use lemmy_db_views::structs::{CommentReportView, CommentView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; /// Creates a comment report and notifies the moderators of the community -#[tracing::instrument(skip(context))] pub async fn create_comment_report( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; - let reason = data.reason.trim().to_string(); - check_report_reason(&reason, &local_site)?; + let slur_regex = slur_regex(&context).await?; + check_report_reason(&reason, &slur_regex)?; let person_id = local_user_view.person.id; let comment_id = data.comment_id; @@ -57,6 +56,7 @@ pub async fn create_comment_report( comment_id, original_comment_text: comment_view.comment.content, reason, + violates_instance_rules: data.violates_instance_rules.unwrap_or_default(), }; let report = CommentReport::report(&mut context.pool(), &report_form) @@ -67,6 +67,7 @@ pub async fn create_comment_report( CommentReportView::read(&mut context.pool(), report.id, person_id).await?; // Email the admins + let local_site = LocalSite::read(&mut context.pool()).await?; if local_site.reports_email_admins { send_new_report_email_to_admins( &comment_report_view.creator.name, diff --git a/crates/api/src/reports/comment_report/resolve.rs b/crates/api/src/reports/comment_report/resolve.rs index 5ab36054f..bed110483 100644 --- a/crates/api/src/reports/comment_report/resolve.rs +++ b/crates/api/src/reports/comment_report/resolve.rs @@ -1,7 +1,9 @@ -use actix_web::web::{Data, Json}; +use activitypub_federation::config::Data; +use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, reports::comment::{CommentReportResponse, ResolveCommentReport}, + send_activity::{ActivityChannel, SendActivityData}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable}; @@ -9,7 +11,6 @@ use lemmy_db_views::structs::{CommentReportView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; /// Resolves or unresolves a comment report and notifies the moderators of the community -#[tracing::instrument(skip(context))] pub async fn resolve_comment_report( data: Json, context: Data, @@ -42,6 +43,16 @@ pub async fn resolve_comment_report( let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id).await?; + ActivityChannel::submit_activity( + SendActivityData::SendResolveReport { + object_id: comment_report_view.comment.ap_id.inner().clone(), + actor: local_user_view.person, + report_creator: report.creator, + community: comment_report_view.community.clone(), + }, + &context, + )?; + Ok(Json(CommentReportResponse { comment_report_view, })) diff --git a/crates/api/src/reports/community_report/create.rs b/crates/api/src/reports/community_report/create.rs new file mode 100644 index 000000000..71338d2d1 --- /dev/null +++ b/crates/api/src/reports/community_report/create.rs @@ -0,0 +1,71 @@ +use crate::check_report_reason; +use actix_web::web::{Data, Json}; +use lemmy_api_common::{ + context::LemmyContext, + reports::community::{CommunityReportResponse, CreateCommunityReport}, + utils::{send_new_report_email_to_admins, slur_regex}, +}; +use lemmy_db_schema::{ + source::{ + community::Community, + community_report::{CommunityReport, CommunityReportForm}, + local_site::LocalSite, + }, + traits::{Crud, Reportable}, +}; +use lemmy_db_views::structs::{CommunityReportView, LocalUserView}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; + +pub async fn create_community_report( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let reason = data.reason.trim().to_string(); + let slur_regex = slur_regex(&context).await?; + check_report_reason(&reason, &slur_regex)?; + + let person_id = local_user_view.person.id; + let community_id = data.community_id; + let community = Community::read(&mut context.pool(), community_id).await?; + + let report_form = CommunityReportForm { + creator_id: person_id, + community_id, + original_community_banner: community.banner, + original_community_description: community.description, + original_community_icon: community.icon, + original_community_name: community.name, + original_community_sidebar: community.sidebar, + original_community_title: community.title, + reason, + }; + + let report = CommunityReport::report(&mut context.pool(), &report_form) + .await + .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; + + let community_report_view = + CommunityReportView::read(&mut context.pool(), report.id, person_id).await?; + + // Email the admins + let local_site = LocalSite::read(&mut context.pool()).await?; + if local_site.reports_email_admins { + send_new_report_email_to_admins( + &community_report_view.creator.name, + // The argument here is normally the reported content's creator, but a community doesn't have + // a single person to be considered the creator or the person responsible for the bad thing, + // so the community name is used instead + &community_report_view.community.name, + &mut context.pool(), + context.settings(), + ) + .await?; + } + + // TODO: consider federating this + + Ok(Json(CommunityReportResponse { + community_report_view, + })) +} diff --git a/crates/api/src/reports/community_report/mod.rs b/crates/api/src/reports/community_report/mod.rs new file mode 100644 index 000000000..c85613aa6 --- /dev/null +++ b/crates/api/src/reports/community_report/mod.rs @@ -0,0 +1,2 @@ +pub mod create; +pub mod resolve; diff --git a/crates/api/src/reports/community_report/resolve.rs b/crates/api/src/reports/community_report/resolve.rs new file mode 100644 index 000000000..73fe1c80b --- /dev/null +++ b/crates/api/src/reports/community_report/resolve.rs @@ -0,0 +1,36 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{ + context::LemmyContext, + reports::community::{CommunityReportResponse, ResolveCommunityReport}, + utils::is_admin, +}; +use lemmy_db_schema::{source::community_report::CommunityReport, traits::Reportable}; +use lemmy_db_views::structs::{CommunityReportView, LocalUserView}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; + +pub async fn resolve_community_report( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + is_admin(&local_user_view)?; + + let report_id = data.report_id; + let person_id = local_user_view.person.id; + if data.resolved { + CommunityReport::resolve(&mut context.pool(), report_id, person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; + } else { + CommunityReport::unresolve(&mut context.pool(), report_id, person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; + } + + let community_report_view = + CommunityReportView::read(&mut context.pool(), report_id, person_id).await?; + + Ok(Json(CommunityReportResponse { + community_report_view, + })) +} diff --git a/crates/api/src/reports/mod.rs b/crates/api/src/reports/mod.rs index f23d1d71f..3bd4629fd 100644 --- a/crates/api/src/reports/mod.rs +++ b/crates/api/src/reports/mod.rs @@ -1,4 +1,5 @@ pub mod comment_report; +pub mod community_report; pub mod post_report; pub mod private_message_report; pub mod report_combined; diff --git a/crates/api/src/reports/post_report/create.rs b/crates/api/src/reports/post_report/create.rs index bc85bdbe7..cf5c479a2 100644 --- a/crates/api/src/reports/post_report/create.rs +++ b/crates/api/src/reports/post_report/create.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ check_community_user_action, check_post_deleted_or_removed, send_new_report_email_to_admins, + slur_regex, }, }; use lemmy_db_schema::{ @@ -22,16 +23,14 @@ use lemmy_db_views::structs::{LocalUserView, PostReportView, PostView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; /// Creates a post report and notifies the moderators of the community -#[tracing::instrument(skip(context))] pub async fn create_post_report( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; - let reason = data.reason.trim().to_string(); - check_report_reason(&reason, &local_site)?; + let slur_regex = slur_regex(&context).await?; + check_report_reason(&reason, &slur_regex)?; let person_id = local_user_view.person.id; let post_id = data.post_id; @@ -53,6 +52,7 @@ pub async fn create_post_report( original_post_url: post_view.post.url, original_post_body: post_view.post.body, reason, + violates_instance_rules: data.violates_instance_rules.unwrap_or_default(), }; let report = PostReport::report(&mut context.pool(), &report_form) @@ -62,6 +62,7 @@ pub async fn create_post_report( let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?; // Email the admins + let local_site = LocalSite::read(&mut context.pool()).await?; if local_site.reports_email_admins { send_new_report_email_to_admins( &post_report_view.creator.name, diff --git a/crates/api/src/reports/post_report/resolve.rs b/crates/api/src/reports/post_report/resolve.rs index 26b182a45..fff6187b0 100644 --- a/crates/api/src/reports/post_report/resolve.rs +++ b/crates/api/src/reports/post_report/resolve.rs @@ -1,7 +1,9 @@ -use actix_web::web::{Data, Json}; +use activitypub_federation::config::Data; +use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, reports::post::{PostReportResponse, ResolvePostReport}, + send_activity::{ActivityChannel, SendActivityData}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable}; @@ -9,7 +11,6 @@ use lemmy_db_views::structs::{LocalUserView, PostReportView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; /// Resolves or unresolves a post report and notifies the moderators of the community -#[tracing::instrument(skip(context))] pub async fn resolve_post_report( data: Json, context: Data, @@ -33,6 +34,7 @@ pub async fn resolve_post_report( .await .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; } else { + // TODO: not federated PostReport::unresolve(&mut context.pool(), report_id, person_id) .await .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; @@ -40,5 +42,15 @@ pub async fn resolve_post_report( let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id).await?; + ActivityChannel::submit_activity( + SendActivityData::SendResolveReport { + object_id: post_report_view.post.ap_id.inner().clone(), + actor: local_user_view.person, + report_creator: report.creator, + community: post_report_view.community.clone(), + }, + &context, + )?; + Ok(Json(PostReportResponse { post_report_view })) } diff --git a/crates/api/src/reports/private_message_report/create.rs b/crates/api/src/reports/private_message_report/create.rs index 17b5dceeb..91295e821 100644 --- a/crates/api/src/reports/private_message_report/create.rs +++ b/crates/api/src/reports/private_message_report/create.rs @@ -3,7 +3,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, reports::private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse}, - utils::send_new_report_email_to_admins, + utils::{send_new_report_email_to_admins, slur_regex}, }; use lemmy_db_schema::{ source::{ @@ -16,16 +16,14 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn create_pm_report( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; - let reason = data.reason.trim().to_string(); - check_report_reason(&reason, &local_site)?; + let slur_regex = slur_regex(&context).await?; + check_report_reason(&reason, &slur_regex)?; let person_id = local_user_view.person.id; let private_message_id = data.private_message_id; @@ -51,6 +49,7 @@ pub async fn create_pm_report( PrivateMessageReportView::read(&mut context.pool(), report.id).await?; // Email the admins + let local_site = LocalSite::read(&mut context.pool()).await?; if local_site.reports_email_admins { send_new_report_email_to_admins( &private_message_report_view.creator.name, diff --git a/crates/api/src/reports/private_message_report/resolve.rs b/crates/api/src/reports/private_message_report/resolve.rs index 3f812e4fe..d15452cde 100644 --- a/crates/api/src/reports/private_message_report/resolve.rs +++ b/crates/api/src/reports/private_message_report/resolve.rs @@ -8,7 +8,6 @@ use lemmy_db_schema::{source::private_message_report::PrivateMessageReport, trai use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn resolve_pm_report( data: Json, context: Data, diff --git a/crates/api/src/reports/report_combined/list.rs b/crates/api/src/reports/report_combined/list.rs index 12548d189..a45409532 100644 --- a/crates/api/src/reports/report_combined/list.rs +++ b/crates/api/src/reports/report_combined/list.rs @@ -4,38 +4,47 @@ use lemmy_api_common::{ reports::combined::{ListReports, ListReportsResponse}, utils::check_community_mod_of_any_or_admin_action, }; -use lemmy_db_views::{report_combined_view::ReportCombinedQuery, structs::LocalUserView}; +use lemmy_db_schema::traits::PaginationCursorBuilder; +use lemmy_db_views::{ + combined::report_combined_view::ReportCombinedQuery, + structs::{LocalUserView, ReportCombinedView}, +}; use lemmy_utils::error::LemmyResult; /// Lists reports for a community if an id is supplied /// or returns all reports for communities a user moderates -#[tracing::instrument(skip(context))] pub async fn list_reports( data: Query, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let community_id = data.community_id; - let unresolved_only = data.unresolved_only; + let my_reports_only = data.my_reports_only; - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; + // Only check mod or admin status when not viewing my reports + if !my_reports_only.unwrap_or_default() { + check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; + } - // parse pagination token - let page_after = if let Some(pa) = &data.page_cursor { - Some(pa.read(&mut context.pool()).await?) + let cursor_data = if let Some(cursor) = &data.page_cursor { + Some(ReportCombinedView::from_cursor(cursor, &mut context.pool()).await?) } else { None }; - let page_back = data.page_back; let reports = ReportCombinedQuery { - community_id, - unresolved_only, - page_after, - page_back, + community_id: data.community_id, + post_id: data.post_id, + type_: data.type_, + unresolved_only: data.unresolved_only, + cursor_data, + page_back: data.page_back, + show_community_rule_violations: data.show_community_rule_violations, + my_reports_only, } .list(&mut context.pool(), &local_user_view) .await?; - Ok(Json(ListReportsResponse { reports })) + let next_page = reports.last().map(PaginationCursorBuilder::to_cursor); + + Ok(Json(ListReportsResponse { reports, next_page })) } diff --git a/crates/api/src/site/admin_allow_instance.rs b/crates/api/src/site/admin_allow_instance.rs index cf3415b5b..2ceb53d21 100644 --- a/crates/api/src/site/admin_allow_instance.rs +++ b/crates/api/src/site/admin_allow_instance.rs @@ -18,7 +18,6 @@ use lemmy_db_schema::{ 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, diff --git a/crates/api/src/site/admin_block_instance.rs b/crates/api/src/site/admin_block_instance.rs index f7b286ee1..11d1968cf 100644 --- a/crates/api/src/site/admin_block_instance.rs +++ b/crates/api/src/site/admin_block_instance.rs @@ -18,7 +18,6 @@ use lemmy_db_schema::{ 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, diff --git a/crates/api/src/site/federated_instances.rs b/crates/api/src/site/federated_instances.rs index 5943cfd9a..dc04801f0 100644 --- a/crates/api/src/site/federated_instances.rs +++ b/crates/api/src/site/federated_instances.rs @@ -7,7 +7,6 @@ use lemmy_api_common::{ use lemmy_db_views::structs::SiteView; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn get_federated_instances( context: Data, ) -> LemmyResult> { diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 042009d24..bb306aeb9 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -12,14 +12,12 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_db_views_actor::structs::PersonView; +use lemmy_db_views::structs::{LocalUserView, PersonView, SiteView}; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, VERSION, }; -#[tracing::instrument(skip(context))] pub async fn leave_admin( context: Data, local_user_view: LocalUserView, @@ -71,8 +69,8 @@ pub async fn leave_admin( version: VERSION.to_string(), all_languages, discussion_languages, - oauth_providers: Some(oauth_providers), - admin_oauth_providers: None, + oauth_providers, + admin_oauth_providers: vec![], blocked_urls, tagline, my_user: None, diff --git a/crates/api/src/site/list_all_media.rs b/crates/api/src/site/list_all_media.rs index 4d8d2dc2a..ed29d9ad7 100644 --- a/crates/api/src/site/list_all_media.rs +++ b/crates/api/src/site/list_all_media.rs @@ -7,7 +7,6 @@ use lemmy_api_common::{ use lemmy_db_views::structs::{LocalImageView, LocalUserView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn list_all_media( data: Query, context: Data, diff --git a/crates/api/src/site/mod_log.rs b/crates/api/src/site/mod_log.rs index 8c6bfdb50..b9412b9d9 100644 --- a/crates/api/src/site/mod_log.rs +++ b/crates/api/src/site/mod_log.rs @@ -4,12 +4,13 @@ use lemmy_api_common::{ site::{GetModlog, GetModlogResponse}, utils::{check_community_mod_of_any_or_admin_action, check_private_instance}, }; -use lemmy_db_schema::source::local_site::LocalSite; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_moderator::{self, modlog_combined_view::ModlogCombinedQuery}; +use lemmy_db_schema::{source::local_site::LocalSite, traits::PaginationCursorBuilder}; +use lemmy_db_views::{ + combined::modlog_combined_view::ModlogCombinedQuery, + structs::{LocalUserView, ModlogCombinedView}, +}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn get_mod_log( data: Query, context: Data, @@ -19,11 +20,8 @@ pub async fn get_mod_log( check_private_instance(&local_user_view, &local_site)?; - let type_ = data.type_; - let community_id = data.community_id; - - let is_mod_or_admin = if let Some(local_user_view) = local_user_view { - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()) + let is_mod_or_admin = if let Some(local_user_view) = &local_user_view { + check_community_mod_of_any_or_admin_action(local_user_view, &mut context.pool()) .await .is_ok() } else { @@ -36,31 +34,30 @@ pub async fn get_mod_log( } else { data.mod_person_id }; - let other_person_id = data.other_person_id; - let post_id = data.post_id; - let comment_id = data.comment_id; - // parse pagination token - let page_after = if let Some(pa) = &data.page_cursor { - Some(pa.read(&mut context.pool()).await?) + let cursor_data = if let Some(cursor) = &data.page_cursor { + Some(ModlogCombinedView::from_cursor(cursor, &mut context.pool()).await?) } else { None }; - let page_back = data.page_back; let modlog = ModlogCombinedQuery { - type_, - community_id, + type_: data.type_, + listing_type: data.listing_type, + community_id: data.community_id, mod_person_id, - other_person_id, - post_id, - comment_id, + other_person_id: data.other_person_id, + local_user: local_user_view.as_ref().map(|u| &u.local_user), + post_id: data.post_id, + comment_id: data.comment_id, hide_modlog_names: Some(hide_modlog_names), - page_after, - page_back, + cursor_data, + page_back: data.page_back, } .list(&mut context.pool()) .await?; - Ok(Json(GetModlogResponse { modlog })) + let next_page = modlog.last().map(PaginationCursorBuilder::to_cursor); + + Ok(Json(GetModlogResponse { modlog, next_page })) } diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index 5208cc397..9fd43107d 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -18,7 +18,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn purge_comment( data: Json, context: Data, diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index c55f753dc..5413f9421 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -17,11 +17,9 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityModeratorView; +use lemmy_db_views::structs::{CommunityModeratorView, LocalUserView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn purge_community( data: Json, context: Data, diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 0f15e7726..e09c77f75 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -19,7 +19,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn purge_person( data: Json, context: Data, diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index e726945f5..c94109eec 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -2,10 +2,9 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, - request::purge_image_from_pictrs, send_activity::{ActivityChannel, SendActivityData}, site::PurgePost, - utils::is_admin, + utils::{is_admin, purge_post_images}, SuccessResponse, }; use lemmy_db_schema::{ @@ -19,7 +18,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn purge_post( data: Json, context: Data, @@ -39,14 +37,7 @@ pub async fn purge_post( ) .await?; - // Purge image - if let Some(url) = &post.url { - purge_image_from_pictrs(url, &context).await.ok(); - } - // Purge thumbnail - if let Some(thumbnail_url) = &post.thumbnail_url { - purge_image_from_pictrs(thumbnail_url, &context).await.ok(); - } + purge_post_images(post.url.clone(), post.thumbnail_url.clone(), &context).await; Post::delete(&mut context.pool(), data.post_id).await?; diff --git a/crates/api/src/site/registration_applications/list.rs b/crates/api/src/site/registration_applications/list.rs index 877e83796..cae0acd43 100644 --- a/crates/api/src/site/registration_applications/list.rs +++ b/crates/api/src/site/registration_applications/list.rs @@ -7,7 +7,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::source::local_site::LocalSite; use lemmy_db_views::{ - registration_application_view::RegistrationApplicationQuery, + registration_applications::registration_application_view::RegistrationApplicationQuery, structs::LocalUserView, }; use lemmy_utils::error::LemmyResult; diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index b9e8a5a76..44ad30a6b 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -19,10 +19,7 @@ workspace = true [features] full = [ "tracing", - "rosetta-i18n", "lemmy_db_views/full", - "lemmy_db_views_actor/full", - "lemmy_db_views_moderator/full", "lemmy_utils/full", "activitypub_federation", "encoding_rs", @@ -37,12 +34,12 @@ full = [ "jsonwebtoken", "mime", "moka", + "actix-web-httpauth", + "webmention", ] [dependencies] lemmy_db_views = { workspace = true } -lemmy_db_views_moderator = { workspace = true } -lemmy_db_views_actor = { workspace = true } lemmy_db_schema = { workspace = true } lemmy_utils = { workspace = true } activitypub_federation = { workspace = true, optional = true } @@ -53,7 +50,6 @@ chrono = { workspace = true } tracing = { workspace = true, optional = true } reqwest-middleware = { workspace = true, optional = true } regex = { workspace = true } -rosetta-i18n = { workspace = true, optional = true } futures = { workspace = true, optional = true } uuid = { workspace = true, optional = true } tokio = { workspace = true, optional = true } @@ -61,17 +57,19 @@ reqwest = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true } moka = { workspace = true, optional = true } anyhow.workspace = true -actix-web = { workspace = true, optional = true } enum-map = { workspace = true } +actix-web = { workspace = true, optional = true } urlencoding = { workspace = true } mime = { version = "0.3.17", optional = true } mime_guess = "2.0.5" -infer = "0.16.0" +infer = "0.19.0" webpage = { version = "2.0", default-features = false, optional = true, features = [ "serde", ] } encoding_rs = { version = "0.8.35", optional = true } -jsonwebtoken = { version = "9.3.0", optional = true } +jsonwebtoken = { version = "9.3.1", optional = true } +actix-web-httpauth = { version = "0.8.2", optional = true } +webmention = { version = "0.6.0", optional = true } [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 0245a0459..a7c8b7afa 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -3,12 +3,7 @@ use crate::{ community::CommunityResponse, context::LemmyContext, post::PostResponse, - utils::{ - check_person_instance_community_block, - get_interface_language, - is_mod_or_admin, - send_email_to_user, - }, + utils::{check_person_instance_community_block, is_mod_or_admin, send_email_to_user}, }; use actix_web::web::Json; use lemmy_db_schema::{ @@ -25,8 +20,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{CommentView, LocalUserView, PostView}; -use lemmy_db_views_actor::structs::CommunityView; +use lemmy_db_views::structs::{CommentView, CommunityView, LocalUserView, PostView}; use lemmy_utils::{ error::LemmyResult, utils::{markdown::markdown_to_html, mention::MentionData}, @@ -92,7 +86,7 @@ pub async fn build_post_response( } // TODO: this function is a mess and should be split up to handle email separately -#[tracing::instrument(skip_all)] + pub async fn send_local_notifs( mentions: Vec, post_or_comment_id: PostOrCommentId, @@ -102,7 +96,6 @@ pub async fn send_local_notifs( local_user_view: Option<&LocalUserView>, ) -> LemmyResult> { let mut recipient_ids = Vec::new(); - let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let (comment_opt, post, community) = match post_or_comment_id { PostOrCommentId::Post(post_id) => { @@ -142,6 +135,8 @@ pub async fn send_local_notifs( } }; + let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); + // Send the local mentions for mention in mentions .iter() @@ -157,7 +152,7 @@ pub async fn send_local_notifs( recipient_ids.push(mention_user_view.local_user.id); // Make the correct reply form depending on whether its a post or comment mention - let comment_content_or_post_body = if let Some(comment) = &comment_opt { + let (link, comment_content_or_post_body) = if let Some(comment) = &comment_opt { let person_comment_mention_form = PersonCommentMentionInsertForm { recipient_id: mention_user_view.person.id, comment_id: comment.id, @@ -169,7 +164,10 @@ pub async fn send_local_notifs( PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form) .await .ok(); - comment.content.clone() + ( + comment.local_url(context.settings())?, + comment.content.clone(), + ) } else { let person_post_mention_form = PersonPostMentionInsertForm { recipient_id: mention_user_view.person.id, @@ -181,17 +179,20 @@ pub async fn send_local_notifs( PersonPostMention::create(&mut context.pool(), &person_post_mention_form) .await .ok(); - post.body.clone().unwrap_or_default() + ( + post.local_url(context.settings())?, + post.body.clone().unwrap_or_default(), + ) }; // Send an email to those local users that have notifications on if do_send_email { - let lang = get_interface_language(&mention_user_view); + let lang = &mention_user_view.local_user.interface_i18n_language(); let content = markdown_to_html(&comment_content_or_post_body); send_email_to_user( &mention_user_view, &lang.notification_mentioned_by_subject(&person.name), - &lang.notification_mentioned_by_body(&content, &inbox_link, &person.name), + &lang.notification_mentioned_by_body(&link, &content, &inbox_link, &person.name), context.settings(), ) .await @@ -239,12 +240,19 @@ pub async fn send_local_notifs( .ok(); if do_send_email { - let lang = get_interface_language(&parent_user_view); + let lang = &parent_user_view.local_user.interface_i18n_language(); let content = markdown_to_html(&comment.content); send_email_to_user( &parent_user_view, &lang.notification_comment_reply_subject(&person.name), - &lang.notification_comment_reply_body(&content, &inbox_link, &person.name), + &lang.notification_comment_reply_body( + comment.local_url(context.settings())?, + &content, + &inbox_link, + &parent_comment.content, + &post.name, + &person.name, + ), context.settings(), ) .await @@ -285,12 +293,18 @@ pub async fn send_local_notifs( .ok(); if do_send_email { - let lang = get_interface_language(&parent_user_view); + let lang = &parent_user_view.local_user.interface_i18n_language(); let content = markdown_to_html(&comment.content); send_email_to_user( &parent_user_view, &lang.notification_post_reply_subject(&person.name), - &lang.notification_post_reply_body(&content, &inbox_link, &person.name), + &lang.notification_post_reply_body( + comment.local_url(context.settings())?, + &content, + &inbox_link, + &post.name, + &person.name, + ), context.settings(), ) .await diff --git a/crates/api_common/src/comment.rs b/crates/api_common/src/comment.rs index 0d416e9f0..d6053b099 100644 --- a/crates/api_common/src/comment.rs +++ b/crates/api_common/src/comment.rs @@ -3,7 +3,7 @@ use lemmy_db_schema::{ CommentSortType, ListingType, }; -use lemmy_db_views::structs::{CommentView, VoteView}; +use lemmy_db_views::structs::{CommentSlimView, CommentView, VoteView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -117,6 +117,10 @@ pub struct GetComments { #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, #[cfg_attr(feature = "full", ts(optional))] + /// Filter to within a given time range, in seconds. + /// IE 60 would give results for the past minute. + pub time_range_seconds: Option, + #[cfg_attr(feature = "full", ts(optional))] pub max_depth: Option, #[cfg_attr(feature = "full", ts(optional))] pub page: Option, @@ -144,6 +148,14 @@ pub struct GetCommentsResponse { pub comments: Vec, } +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A slimmer comment list response, without the post or community. +pub struct GetCommentsSlimResponse { + pub comments: Vec, +} + #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index 64643e4a8..3bd6c9b1a 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -4,7 +4,7 @@ use lemmy_db_schema::{ CommunityVisibility, ListingType, }; -use lemmy_db_views_actor::structs::{ +use lemmy_db_views::structs::{ CommunityModeratorView, CommunitySortType, CommunityView, @@ -97,6 +97,10 @@ pub struct ListCommunities { #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, #[cfg_attr(feature = "full", ts(optional))] + /// Filter to within a given time range, in seconds. + /// IE 60 would give results for the past minute. + pub time_range_seconds: Option, + #[cfg_attr(feature = "full", ts(optional))] pub show_nsfw: Option, #[cfg_attr(feature = "full", ts(optional))] pub page: Option, diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index dd1c0a68a..7eac9b21e 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -24,8 +24,6 @@ pub mod utils; pub extern crate lemmy_db_schema; pub extern crate lemmy_db_views; -pub extern crate lemmy_db_views_actor; -pub extern crate lemmy_db_views_moderator; pub extern crate lemmy_utils; pub use lemmy_utils::error::LemmyErrorType; @@ -35,7 +33,7 @@ use std::{cmp::min, time::Duration}; #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(ts_rs::TS))] #[cfg_attr(feature = "full", ts(export))] -/// Saves settings for your user. +/// A response that completes successfully. pub struct SuccessResponse { pub success: bool, } diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 67401663f..f8aea0300 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -3,6 +3,7 @@ use lemmy_db_schema::{ CommentReplyId, CommunityId, LanguageId, + PaginationCursor, PersonCommentMentionId, PersonId, PersonPostMentionId, @@ -17,15 +18,11 @@ use lemmy_db_schema::{ PostSortType, }; use lemmy_db_views::structs::{ - LocalImageView, - PersonContentCombinedPaginationCursor, - PersonContentCombinedView, - PersonSavedCombinedPaginationCursor, -}; -use lemmy_db_views_actor::structs::{ CommunityModeratorView, - InboxCombinedPaginationCursor, InboxCombinedView, + LocalImageView, + PersonContentCombinedView, + PersonSavedCombinedView, PersonView, }; use serde::{Deserialize, Serialize}; @@ -122,6 +119,9 @@ pub struct SaveUserSettings { /// The default post sort, usually "active" #[cfg_attr(feature = "full", ts(optional))] pub default_post_sort_type: Option, + /// A default time range limit to apply to post sorts, in seconds. 0 means none. + #[cfg_attr(feature = "full", ts(optional))] + pub default_post_time_range_seconds: Option, /// The default comment sort, usually "hot" #[cfg_attr(feature = "full", ts(optional))] pub default_comment_sort_type: Option, @@ -263,7 +263,7 @@ pub struct ListPersonContent { #[cfg_attr(feature = "full", ts(optional))] pub username: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_back: Option, } @@ -275,6 +275,9 @@ pub struct ListPersonContent { /// A person's content response. pub struct ListPersonContentResponse { pub content: Vec, + /// the pagination cursor to use to fetch the next page + #[cfg_attr(feature = "full", ts(optional))] + pub next_page: Option, } #[skip_serializing_none] @@ -286,7 +289,7 @@ pub struct ListPersonSaved { #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_back: Option, } @@ -297,7 +300,10 @@ pub struct ListPersonSaved { #[cfg_attr(feature = "full", ts(export))] /// A person's saved content response. pub struct ListPersonSavedResponse { - pub saved: Vec, + pub saved: Vec, + /// the pagination cursor to use to fetch the next page + #[cfg_attr(feature = "full", ts(optional))] + pub next_page: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)] @@ -385,7 +391,7 @@ pub struct ListInbox { #[cfg_attr(feature = "full", ts(optional))] pub unread_only: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_back: Option, } @@ -396,6 +402,9 @@ pub struct ListInbox { /// Get your inbox (replies, comment mentions, post mentions, and messages) pub struct ListInboxResponse { pub inbox: Vec, + /// the pagination cursor to use to fetch the next page + #[cfg_attr(feature = "full", ts(optional))] + pub next_page: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] @@ -534,3 +543,11 @@ pub struct ListMediaResponse { pub struct ListLoginsResponse { pub logins: Vec, } + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Make a request to resend your verification email. +pub struct ResendVerificationEmail { + pub email: SensitiveString, +} diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index a26d6265a..de15e3dd3 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -4,8 +4,7 @@ use lemmy_db_schema::{ PostFeatureType, PostSortType, }; -use lemmy_db_views::structs::{PaginationCursor, PostView, VoteView}; -use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; +use lemmy_db_views::structs::{CommunityView, PostPaginationCursor, PostView, VoteView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -72,7 +71,6 @@ pub struct GetPost { pub struct GetPostResponse { pub post_view: PostView, pub community_view: CommunityView, - pub moderators: Vec, /// A list of cross-posts, or other times / communities this link has been posted to. pub cross_posts: Vec, } @@ -87,6 +85,11 @@ pub struct GetPosts { pub type_: Option, #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, + #[cfg_attr(feature = "full", ts(optional))] + /// Filter to within a given time range, in seconds. + /// IE 60 would give results for the past minute. + /// Use Zero to override the local_site and local_user time_range. + pub time_range_seconds: Option, /// DEPRECATED, use page_cursor #[cfg_attr(feature = "full", ts(optional))] pub page: Option, @@ -122,7 +125,7 @@ pub struct GetPosts { /// If true, then only show posts with no comments pub no_comments_only: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_back: Option, } @@ -136,7 +139,7 @@ pub struct GetPostsResponse { pub posts: Vec, /// the pagination cursor to use to fetch the next page #[cfg_attr(feature = "full", ts(optional))] - pub next_page: Option, + pub next_page: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] diff --git a/crates/api_common/src/private_message.rs b/crates/api_common/src/private_message.rs index f8134ea27..aac27499a 100644 --- a/crates/api_common/src/private_message.rs +++ b/crates/api_common/src/private_message.rs @@ -1,5 +1,5 @@ use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId}; -use lemmy_db_views_actor::structs::PrivateMessageView; +use lemmy_db_views::structs::PrivateMessageView; use serde::{Deserialize, Serialize}; #[cfg(feature = "full")] use ts_rs::TS; diff --git a/crates/api_common/src/reports/combined.rs b/crates/api_common/src/reports/combined.rs index 69d928830..36683f812 100644 --- a/crates/api_common/src/reports/combined.rs +++ b/crates/api_common/src/reports/combined.rs @@ -1,5 +1,8 @@ -use lemmy_db_schema::newtypes::CommunityId; -use lemmy_db_views::structs::{ReportCombinedPaginationCursor, ReportCombinedView}; +use lemmy_db_schema::{ + newtypes::{CommunityId, PaginationCursor, PostId}, + ReportType, +}; +use lemmy_db_views::structs::ReportCombinedView; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -14,13 +17,25 @@ pub struct ListReports { /// Only shows the unresolved reports #[cfg_attr(feature = "full", ts(optional))] pub unresolved_only: Option, + /// Filter the type of report. + #[cfg_attr(feature = "full", ts(optional))] + pub type_: Option, + /// Filter by the post id. Can return either comment or post reports. + #[cfg_attr(feature = "full", ts(optional))] + pub post_id: Option, /// if no community is given, it returns reports for all communities moderated by the auth user #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_back: Option, + /// Only for admins: also show reports with `violates_instance_rules=false` + #[cfg_attr(feature = "full", ts(optional))] + pub show_community_rule_violations: Option, + /// If true, view all your created reports. Works for non-admins/mods also. + #[cfg_attr(feature = "full", ts(optional))] + pub my_reports_only: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -29,4 +44,7 @@ pub struct ListReports { /// The post reports response. pub struct ListReportsResponse { pub reports: Vec, + /// the pagination cursor to use to fetch the next page + #[cfg_attr(feature = "full", ts(optional))] + pub next_page: Option, } diff --git a/crates/api_common/src/reports/comment.rs b/crates/api_common/src/reports/comment.rs index d1a51a6a8..fa9b8f991 100644 --- a/crates/api_common/src/reports/comment.rs +++ b/crates/api_common/src/reports/comment.rs @@ -11,6 +11,8 @@ use ts_rs::TS; pub struct CreateCommentReport { pub comment_id: CommentId, pub reason: String, + #[cfg_attr(feature = "full", ts(optional))] + pub violates_instance_rules: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/api_common/src/reports/community.rs b/crates/api_common/src/reports/community.rs new file mode 100644 index 000000000..1b0eceb02 --- /dev/null +++ b/crates/api_common/src/reports/community.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{CommunityId, CommunityReportId}; +use lemmy_db_views::structs::CommunityReportView; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a report for a community. +pub struct CreateCommunityReport { + pub community_id: CommunityId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A community report response. +pub struct CommunityReportResponse { + pub community_report_view: CommunityReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a community report. +pub struct ResolveCommunityReport { + pub report_id: CommunityReportId, + pub resolved: bool, +} diff --git a/crates/api_common/src/reports/mod.rs b/crates/api_common/src/reports/mod.rs index 6584de1bc..a7139bc03 100644 --- a/crates/api_common/src/reports/mod.rs +++ b/crates/api_common/src/reports/mod.rs @@ -1,4 +1,5 @@ pub mod combined; pub mod comment; +pub mod community; pub mod post; pub mod private_message; diff --git a/crates/api_common/src/reports/post.rs b/crates/api_common/src/reports/post.rs index a4d20d575..f2f6637c0 100644 --- a/crates/api_common/src/reports/post.rs +++ b/crates/api_common/src/reports/post.rs @@ -11,6 +11,8 @@ use ts_rs::TS; pub struct CreatePostReport { pub post_id: PostId, pub reason: String, + #[cfg_attr(feature = "full", ts(optional))] + pub violates_instance_rules: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index a70a685ef..d07171800 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -10,25 +10,28 @@ use chrono::{DateTime, Utc}; use encoding_rs::{Encoding, UTF_8}; use futures::StreamExt; use lemmy_db_schema::source::{ - images::{ImageDetailsForm, LocalImage, LocalImageForm}, + images::{ImageDetailsInsertForm, LocalImage, LocalImageForm}, post::{Post, PostUpdateForm}, site::Site, }; use lemmy_utils::{ - error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, + error::{FederationError, LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, settings::structs::{PictrsImageMode, Settings}, REQWEST_TIMEOUT, VERSION, }; use mime::{Mime, TEXT_HTML}; use reqwest::{ - header::{CONTENT_TYPE, RANGE}, + header::{CONTENT_TYPE, LOCATION, RANGE}, + redirect::Policy, Client, ClientBuilder, Response, }; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; +use std::net::IpAddr; +use tokio::net::lookup_host; use tracing::{info, warn}; use url::Url; use urlencoding::encode; @@ -41,12 +44,44 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder { .user_agent(user_agent.clone()) .timeout(REQWEST_TIMEOUT) .connect_timeout(REQWEST_TIMEOUT) + .redirect(Policy::none()) .use_rustls_tls() } /// Fetches metadata for the given link and optionally generates thumbnail. -#[tracing::instrument(skip_all)] -pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult { +pub async fn fetch_link_metadata( + url: &Url, + context: &LemmyContext, + recursion: bool, +) -> LemmyResult { + if url.scheme() != "http" && url.scheme() != "https" { + return Err(LemmyErrorType::InvalidUrl.into()); + } + + // Resolve the domain and throw an error if it points to any internal IP, + // using logic from nightly IpAddr::is_global. + if !cfg!(debug_assertions) { + // TODO: Replace with IpAddr::is_global() once stabilized + // https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global + let domain = url.domain().ok_or(FederationError::UrlWithoutDomain)?; + let invalid_ip = lookup_host((domain.to_owned(), 80)) + .await? + .any(|addr| match addr.ip() { + IpAddr::V4(addr) => { + addr.is_private() || addr.is_link_local() || addr.is_loopback() || addr.is_multicast() + } + IpAddr::V6(addr) => { + addr.is_loopback() + || addr.is_multicast() + || ((addr.segments()[0] & 0xfe00) == 0xfc00) // is_unique_local + || ((addr.segments()[0] & 0xffc0) == 0xfe80) // is_unicast_link_local + } + }); + if invalid_ip { + return Err(LemmyErrorType::InvalidUrl.into()); + } + } + info!("Fetching site metadata for url: {}", url); // We only fetch the first MB of data in order to not waste bandwidth especially for large // binary files. This high limit is particularly needed for youtube, which includes a lot of @@ -63,6 +98,16 @@ pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResu .await? .error_for_status()?; + // Manually follow one redirect, using internal IP check. Further redirects are ignored. + let location = response + .headers() + .get(LOCATION) + .and_then(|l| l.to_str().ok()); + if let (Some(location), false) = (location, recursion) { + let url = location.parse()?; + return Box::pin(fetch_link_metadata(&url, context, true)).await; + } + let mut content_type: Option = response .headers() .get(CONTENT_TYPE) @@ -150,7 +195,9 @@ pub async fn generate_post_link_metadata( context: Data, ) -> LemmyResult<()> { let metadata = match &post.url { - Some(url) => fetch_link_metadata(url, &context).await.unwrap_or_default(), + Some(url) => fetch_link_metadata(url, &context, false) + .await + .unwrap_or_default(), _ => Default::default(), }; @@ -292,17 +339,19 @@ pub struct PictrsFileDetails { pub height: u16, pub content_type: String, pub created_at: DateTime, + pub blurhash: Option, } impl PictrsFileDetails { /// Builds the image form. This should always use the thumbnail_url, /// Because the post_view joins to it - pub fn build_image_details_form(&self, thumbnail_url: &Url) -> ImageDetailsForm { - ImageDetailsForm { + pub fn build_image_details_form(&self, thumbnail_url: &Url) -> ImageDetailsInsertForm { + ImageDetailsInsertForm { link: thumbnail_url.clone().into(), width: self.width.into(), height: self.height.into(), content_type: self.content_type.clone(), + blurhash: self.blurhash.clone(), } } } @@ -374,7 +423,6 @@ pub async fn delete_image_from_pictrs(alias: &str, context: &LemmyContext) -> Le } /// Retrieves the image with local pict-rs and generates a thumbnail. Returns the thumbnail url. -#[tracing::instrument(skip_all)] async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> LemmyResult { let pictrs_config = context.settings().pictrs()?; @@ -428,7 +476,6 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L /// Fetches the image details for pictrs proxied images /// /// We don't need to check for image mode, as that's already been done -#[tracing::instrument(skip_all)] pub async fn fetch_pictrs_proxied_image_details( image_url: &Url, context: &LemmyContext, @@ -464,7 +511,7 @@ pub async fn fetch_pictrs_proxied_image_details( } // TODO: get rid of this by reading content type from db -#[tracing::instrument(skip_all)] + async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> LemmyResult<()> { let response = client.get(url.as_str()).send().await?; if response @@ -498,7 +545,7 @@ mod tests { async fn test_link_metadata() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ")?; - let sample_res = fetch_link_metadata(&sample_url, &context).await?; + let sample_res = fetch_link_metadata(&sample_url, &context, false).await?; assert_eq!( Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()), sample_res.opengraph_data.title diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index 07203ffe4..fdefbaac0 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ private_message::PrivateMessage, }, }; -use lemmy_db_views_actor::structs::PrivateMessageView; +use lemmy_db_views::structs::PrivateMessageView; use lemmy_utils::error::LemmyResult; use std::sync::{LazyLock, OnceLock}; use tokio::{ @@ -99,6 +99,12 @@ pub enum SendActivityData { community: Community, reason: String, }, + SendResolveReport { + object_id: Url, + actor: Person, + report_creator: Person, + community: Community, + }, } // TODO: instead of static, move this into LemmyContext. make sure that stopping the process with diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 8dfb1132b..307b9a4e9 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -6,6 +6,7 @@ use lemmy_db_schema::{ CommunityId, InstanceId, LanguageId, + PaginationCursor, PersonId, PostId, RegistrationApplicationId, @@ -27,22 +28,22 @@ use lemmy_db_schema::{ PostListingMode, PostSortType, RegistrationMode, + SearchSortType, SearchType, }; use lemmy_db_views::structs::{ CommentView, - LocalUserView, - PostView, - RegistrationApplicationView, - SiteView, -}; -use lemmy_db_views_actor::structs::{ CommunityFollowerView, CommunityModeratorView, CommunityView, + LocalUserView, + ModlogCombinedView, PersonView, + PostView, + RegistrationApplicationView, + SearchCombinedView, + SiteView, }; -use lemmy_db_views_moderator::structs::{ModlogCombinedPaginationCursor, ModlogCombinedView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -52,9 +53,10 @@ use ts_rs::TS; #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// Searches the site, given a query string, and some optional filters. +/// Searches the site, given a search term, and some optional filters. pub struct Search { - pub q: String, + #[cfg_attr(feature = "full", ts(optional))] + pub search_term: Option, #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, #[cfg_attr(feature = "full", ts(optional))] @@ -64,14 +66,14 @@ pub struct Search { #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, #[cfg_attr(feature = "full", ts(optional))] - pub sort: Option, + pub sort: Option, + #[cfg_attr(feature = "full", ts(optional))] + /// Filter to within a given time range, in seconds. + /// IE 60 would give results for the past minute. + pub time_range_seconds: Option, #[cfg_attr(feature = "full", ts(optional))] pub listing_type: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - #[cfg_attr(feature = "full", ts(optional))] pub title_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub post_url_only: Option, @@ -79,19 +81,21 @@ pub struct Search { pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// The search response, containing lists of the return type possibilities -// TODO this should be redone as a list of tagged enums pub struct SearchResponse { - pub type_: SearchType, - pub comments: Vec, - pub posts: Vec, - pub communities: Vec, - pub users: Vec, + pub results: Vec, + /// the pagination cursor to use to fetch the next page + #[cfg_attr(feature = "full", ts(optional))] + pub next_page: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] @@ -126,20 +130,30 @@ pub struct ResolveObjectResponse { #[cfg_attr(feature = "full", ts(export))] /// Fetches the modlog. pub struct GetModlog { + /// Filter by the moderator. #[cfg_attr(feature = "full", ts(optional))] pub mod_person_id: Option, + /// Filter by the community. #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + /// Filter by the modlog action type. #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, + /// Filter by listing type. When not using All, it will remove the non-community modlog entries, + /// such as site bans, instance blocks, adding an admin, etc. + #[cfg_attr(feature = "full", ts(optional))] + pub listing_type: Option, + /// Filter by the other / modded person. #[cfg_attr(feature = "full", ts(optional))] pub other_person_id: Option, + /// Filter by post. Will include comments of that post. #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, + /// Filter by comment. #[cfg_attr(feature = "full", ts(optional))] pub comment_id: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_back: Option, } @@ -150,6 +164,9 @@ pub struct GetModlog { /// The modlog fetch response. pub struct GetModlogResponse { pub modlog: Vec, + /// the pagination cursor to use to fetch the next page + #[cfg_attr(feature = "full", ts(optional))] + pub next_page: Option, } #[skip_serializing_none] @@ -181,6 +198,8 @@ pub struct CreateSite { #[cfg_attr(feature = "full", ts(optional))] pub default_post_sort_type: Option, #[cfg_attr(feature = "full", ts(optional))] + pub default_post_time_range_seconds: Option, + #[cfg_attr(feature = "full", ts(optional))] pub default_comment_sort_type: Option, #[cfg_attr(feature = "full", ts(optional))] pub legal_information: Option, @@ -240,6 +259,8 @@ pub struct CreateSite { pub comment_downvotes: Option, #[cfg_attr(feature = "full", ts(optional))] pub disable_donation_dialog: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub disallow_nsfw_content: Option, } #[skip_serializing_none] @@ -280,6 +301,9 @@ pub struct EditSite { /// The default post sort, usually "active" #[cfg_attr(feature = "full", ts(optional))] pub default_post_sort_type: Option, + /// A default time range limit to apply to post sorts, in seconds. 0 means none. + #[cfg_attr(feature = "full", ts(optional))] + pub default_post_time_range_seconds: Option, /// The default comment sort, usually "hot" #[cfg_attr(feature = "full", ts(optional))] pub default_comment_sort_type: Option, @@ -371,6 +395,9 @@ pub struct EditSite { /// donations. #[cfg_attr(feature = "full", ts(optional))] pub disable_donation_dialog: Option, + /// Block NSFW content being created + #[cfg_attr(feature = "full", ts(optional))] + pub disallow_nsfw_content: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -400,10 +427,8 @@ pub struct GetSiteResponse { #[cfg_attr(feature = "full", ts(optional))] pub tagline: Option, /// A list of external auth methods your site supports. - #[cfg_attr(feature = "full", ts(optional))] - pub oauth_providers: Option>, - #[cfg_attr(feature = "full", ts(optional))] - pub admin_oauth_providers: Option>, + pub oauth_providers: Vec, + pub admin_oauth_providers: Vec, pub blocked_urls: Vec, // If true then uploads for post images or markdown images are disabled. Only avatars, icons and // banners can be set. diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 528f408d2..ef909aac8 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -1,4 +1,5 @@ use crate::{ + claims::Claims, context::LemmyContext, request::{ delete_image_from_pictrs, @@ -7,10 +8,11 @@ use crate::{ }, site::{FederatedInstances, InstanceWithFederationState}, }; +use actix_web::{http::header::Header, HttpRequest}; +use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; use chrono::{DateTime, Days, Local, TimeZone, Utc}; use enum_map::{enum_map, EnumMap}; use lemmy_db_schema::{ - aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId, PostOrCommentId}, source::{ comment::{Comment, CommentLike, CommentUpdateForm}, @@ -23,6 +25,7 @@ use lemmy_db_schema::{ local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_site_url_blocklist::LocalSiteUrlBlocklist, + local_user::LocalUser, mod_log::moderator::{ ModRemoveComment, ModRemoveCommentForm, @@ -34,51 +37,56 @@ use lemmy_db_schema::{ person::{Person, PersonUpdateForm}, person_block::PersonBlock, post::{Post, PostLike}, + post_actions::{PostActions, PostActionsForm}, + private_message::PrivateMessage, registration_application::RegistrationApplication, site::Site, }, traits::{Crud, Likeable}, utils::DbPool, + CommunityVisibility, FederationMode, RegistrationMode, }; use lemmy_db_views::{ - comment_view::CommentQuery, - structs::{LocalImageView, LocalUserView, SiteView}, -}; -use lemmy_db_views_actor::structs::{ - CommunityFollowerView, - CommunityModeratorView, - CommunityPersonBanView, - CommunityView, + comment::comment_view::CommentQuery, + structs::{ + CommunityFollowerView, + CommunityModeratorView, + CommunityPersonBanView, + CommunityView, + LocalImageView, + LocalUserView, + SiteView, + }, }; use lemmy_utils::{ - email::{send_email, translations::Lang}, - error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, + email::send_email, + error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult}, rate_limit::{ActionType, BucketConfig}, settings::{ structs::{PictrsImageMode, Settings}, SETTINGS, }, + spawn_try_task, utils::{ markdown::{image_links::markdown_rewrite_image_links, markdown_check_for_blocked_urls}, - slurs::{build_slur_regex, remove_slurs}, - validation::clean_urls_in_text, + slurs::remove_slurs, + validation::{build_and_check_regex, clean_urls_in_text}, }, CacheLock, CACHE_DURATION_FEDERATION, }; use moka::future::Cache; use regex::{escape, Regex, RegexSet}; -use rosetta_i18n::{Language, LanguageId}; use std::sync::LazyLock; -use tracing::warn; +use tracing::{warn, Instrument}; use url::{ParseError, Url}; use urlencoding::encode; +use webmention::{Webmention, WebmentionError}; pub const AUTH_COOKIE_NAME: &str = "jwt"; -#[tracing::instrument(skip_all)] pub async fn is_mod_or_admin( pool: &mut DbPool<'_>, person: &Person, @@ -88,7 +96,6 @@ pub async fn is_mod_or_admin( CommunityView::check_is_mod_or_admin(pool, person.id, community_id).await } -#[tracing::instrument(skip_all)] pub async fn is_mod_or_admin_opt( pool: &mut DbPool<'_>, local_user_view: Option<&LocalUserView>, @@ -108,7 +115,6 @@ pub async fn is_mod_or_admin_opt( /// Check that a person is either a mod of any community, or an admin /// /// Should only be used for read operations -#[tracing::instrument(skip_all)] pub async fn check_community_mod_of_any_or_admin_action( local_user_view: &LocalUserView, pool: &mut DbPool<'_>, @@ -146,20 +152,19 @@ pub fn is_top_mod( } /// Updates the read comment count for a post. Usually done when reading or creating a new comment. -#[tracing::instrument(skip_all)] pub async fn update_read_comments( person_id: PersonId, post_id: PostId, read_comments: i64, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { - let person_post_agg_form = PersonPostAggregatesForm { + let person_post_agg_form = PostActionsForm { person_id, post_id, read_comments, }; - PersonPostAggregates::upsert(pool, &person_post_agg_form).await?; + PostActions::upsert(pool, &person_post_agg_form).await?; Ok(()) } @@ -277,7 +282,6 @@ pub fn check_comment_deleted_or_removed(comment: &Comment) -> LemmyResult<()> { } } -#[tracing::instrument(skip_all)] pub async fn check_person_instance_community_block( my_id: PersonId, potential_blocker_id: PersonId, @@ -291,7 +295,6 @@ pub async fn check_person_instance_community_block( Ok(()) } -#[tracing::instrument(skip_all)] pub async fn check_local_vote_mode( score: i16, post_or_comment_id: PostOrCommentId, @@ -320,7 +323,6 @@ pub async fn check_local_vote_mode( } /// Dont allow bots to do certain actions, like voting -#[tracing::instrument(skip_all)] pub fn check_bot_account(person: &Person) -> LemmyResult<()> { if person.bot_account { Err(LemmyErrorType::InvalidBotAction)? @@ -329,7 +331,6 @@ pub fn check_bot_account(person: &Person) -> LemmyResult<()> { } } -#[tracing::instrument(skip_all)] pub fn check_private_instance( local_user_view: &Option, local_site: &LocalSite, @@ -342,7 +343,6 @@ pub fn check_private_instance( } /// If private messages are disabled, dont allow them to be sent / received -#[tracing::instrument(skip_all)] pub fn check_private_messages_enabled(local_user_view: &LocalUserView) -> Result<(), LemmyError> { if !local_user_view.local_user.enable_private_messages { Err(LemmyErrorType::CouldntCreatePrivateMessage)? @@ -351,7 +351,6 @@ pub fn check_private_messages_enabled(local_user_view: &LocalUserView) -> Result } } -#[tracing::instrument(skip_all)] pub async fn build_federated_instances( local_site: &LocalSite, pool: &mut DbPool<'_>, @@ -447,7 +446,7 @@ pub async fn send_password_reset_email( .email .clone() .ok_or(LemmyErrorType::EmailRequired)?; - let lang = get_interface_language(user); + let lang = &user.local_user.interface_i18n_language(); let subject = &lang.password_reset_subject(&user.person.name); let protocol_and_hostname = settings.get_protocol_and_hostname(); let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token); @@ -463,13 +462,15 @@ pub async fn send_password_reset_email( /// Send a verification email pub async fn send_verification_email( - user: &LocalUserView, + local_site: &LocalSite, + local_user: &LocalUser, + person: &Person, new_email: &str, pool: &mut DbPool<'_>, settings: &Settings, ) -> LemmyResult<()> { let form = EmailVerificationForm { - local_user_id: user.local_user.id, + local_user_id: local_user.id, email: new_email.to_string(), verification_token: uuid::Uuid::new_v4().to_string(), }; @@ -480,29 +481,45 @@ pub async fn send_verification_email( ); EmailVerification::create(pool, &form).await?; - let lang = get_interface_language(user); + let lang = local_user.interface_i18n_language(); let subject = lang.verify_email_subject(&settings.hostname); - let body = lang.verify_email_body(&settings.hostname, &user.person.name, verify_link); - send_email(&subject, new_email, &user.person.name, &body, settings).await?; - Ok(()) + // If an application is required, use a translation that includes that warning. + let body = if local_site.registration_mode == RegistrationMode::RequireApplication { + lang.verify_email_body_with_application(&settings.hostname, &person.name, verify_link) + } else { + lang.verify_email_body(&settings.hostname, &person.name, verify_link) + }; + + send_email(&subject, new_email, &person.name, &body, settings).await } -pub fn get_interface_language(user: &LocalUserView) -> Lang { - lang_str_to_lang(&user.local_user.interface_language) -} +/// Returns true if email was sent. +pub async fn send_verification_email_if_required( + context: &LemmyContext, + local_site: &LocalSite, + local_user: &LocalUser, + person: &Person, +) -> LemmyResult { + let email = &local_user + .email + .clone() + .ok_or(LemmyErrorType::EmailRequired)?; -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(|| { - let en = LanguageId::new("en"); - Lang::from_language_id(&en).expect("default language") - }) + if !local_user.admin && local_site.require_email_verification && !local_user.email_verified { + send_verification_email( + local_site, + local_user, + person, + email, + &mut context.pool(), + context.settings(), + ) + .await?; + Ok(true) + } else { + Ok(false) + } } pub fn local_site_rate_limit_to_rate_limit_config( @@ -523,15 +540,22 @@ pub fn local_site_rate_limit_to_rate_limit_config( }) } -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> { - local_site - .as_ref() - .map(local_site_to_slur_regex) - .unwrap_or(None) +pub async fn slur_regex(context: &LemmyContext) -> LemmyResult { + static CACHE: CacheLock = LazyLock::new(|| { + Cache::builder() + .max_capacity(1) + .time_to_live(CACHE_DURATION_FEDERATION) + .build() + }); + Ok( + CACHE + .try_get_with((), async { + let local_site = LocalSite::read(&mut context.pool()).await.ok(); + build_and_check_regex(local_site.and_then(|s| s.slur_filter_regex).as_deref()) + }) + .await + .map_err(|e| anyhow::anyhow!("Failed to construct regex: {e}"))?, + ) } pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult { @@ -569,8 +593,8 @@ pub async fn send_application_approved_email( .email .clone() .ok_or(LemmyErrorType::EmailRequired)?; - let lang = get_interface_language(user); - let subject = lang.registration_approved_subject(&user.person.actor_id); + let lang = &user.local_user.interface_i18n_language(); + let subject = lang.registration_approved_subject(&user.person.ap_id); let body = lang.registration_approved_body(&settings.hostname); send_email(&subject, email, &user.person.name, &body, settings).await } @@ -595,7 +619,7 @@ pub async fn send_new_applicant_email_to_admins( .email .clone() .ok_or(LemmyErrorType::EmailRequired)?; - let lang = get_interface_language_from_settings(admin); + let lang = &admin.local_user.interface_i18n_language(); let subject = lang.new_application_subject(&settings.hostname, applicant_username); let body = lang.new_application_body(applications_link); send_email(&subject, email, &admin.person.name, &body, settings).await?; @@ -617,7 +641,7 @@ pub async fn send_new_report_email_to_admins( for admin in &admins { if let Some(email) = &admin.local_user.email { - let lang = get_interface_language_from_settings(admin); + let lang = &admin.local_user.interface_i18n_language(); let subject = lang.new_report_subject(&settings.hostname, reported_username, reporter_username); let body = lang.new_report_body(reports_link); @@ -635,18 +659,42 @@ pub fn check_private_instance_and_federation_enabled(local_site: &LocalSite) -> } } -/// Read the site for an actor_id. +pub fn check_nsfw_allowed(nsfw: Option, local_site: Option<&LocalSite>) -> LemmyResult<()> { + let is_nsfw = nsfw.unwrap_or_default(); + let nsfw_disallowed = local_site.is_some_and(|s| s.disallow_nsfw_content); + + if nsfw_disallowed && is_nsfw { + Err(LemmyErrorType::NsfwNotAllowed)? + } + + Ok(()) +} + +/// Read the site for an ap_id. /// /// Used for GetCommunityResponse and GetPersonDetails pub async fn read_site_for_actor( - actor_id: DbUrl, + ap_id: DbUrl, context: &LemmyContext, ) -> LemmyResult> { - let site_id = Site::instance_actor_id_from_url(actor_id.clone().into()); + let site_id = Site::instance_ap_id_from_url(ap_id.clone().into()); let site = Site::read_from_apub_id(&mut context.pool(), &site_id.into()).await?; Ok(site) } +pub async fn purge_post_images( + url: Option, + thumbnail_url: Option, + context: &LemmyContext, +) { + if let Some(url) = url { + purge_image_from_pictrs(&url, context).await.ok(); + } + if let Some(thumbnail_url) = thumbnail_url { + purge_image_from_pictrs(&thumbnail_url, context).await.ok(); + } +} + pub async fn purge_image_posts_for_person( banned_person_id: PersonId, context: &LemmyContext, @@ -654,12 +702,7 @@ pub async fn purge_image_posts_for_person( let pool = &mut context.pool(); let posts = Post::fetch_pictrs_posts_for_creator(pool, banned_person_id).await?; for post in posts { - if let Some(url) = post.url { - purge_image_from_pictrs(&url, context).await.ok(); - } - if let Some(thumbnail_url) = post.thumbnail_url { - purge_image_from_pictrs(&thumbnail_url, context).await.ok(); - } + purge_post_images(post.url, post.thumbnail_url, context).await; } Post::remove_pictrs_post_images_and_thumbnails_for_creator(pool, banned_person_id).await?; @@ -691,12 +734,7 @@ pub async fn purge_image_posts_for_community( let pool = &mut context.pool(); let posts = Post::fetch_pictrs_posts_for_community(pool, banned_community_id).await?; for post in posts { - if let Some(url) = post.url { - purge_image_from_pictrs(&url, context).await.ok(); - } - if let Some(thumbnail_url) = post.thumbnail_url { - purge_image_from_pictrs(&thumbnail_url, context).await.ok(); - } + purge_post_images(post.url, post.thumbnail_url, context).await; } Post::remove_pictrs_post_images_and_thumbnails_for_community(pool, banned_community_id).await?; @@ -809,6 +847,9 @@ pub async fn remove_or_restore_user_data( ) .await?; + // Private messages + PrivateMessage::update_removed_for_creator(pool, banned_person_id, removed).await?; + Ok(()) } @@ -956,33 +997,8 @@ pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> Ok(()) } -pub enum EndpointType { - Community, - Person, - Post, - Comment, - PrivateMessage, -} - -/// Generates an apub endpoint for a given domain, IE xyz.tld -pub fn generate_local_apub_endpoint( - endpoint_type: EndpointType, - name: &str, - domain: &str, -) -> Result { - let point = match endpoint_type { - EndpointType::Community => "c", - EndpointType::Person => "u", - EndpointType::Post => "post", - EndpointType::Comment => "comment", - EndpointType::PrivateMessage => "private_message", - }; - - Ok(Url::parse(&format!("{domain}/{point}/{name}"))?.into()) -} - -pub fn generate_followers_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/followers"))?.into()) +pub fn generate_followers_url(ap_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{ap_id}/followers"))?.into()) } pub fn generate_inbox_url() -> LemmyResult { @@ -990,12 +1006,12 @@ pub fn generate_inbox_url() -> LemmyResult { Ok(Url::parse(&url)?.into()) } -pub fn generate_outbox_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/outbox"))?.into()) +pub fn generate_outbox_url(ap_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{ap_id}/outbox"))?.into()) } -pub fn generate_featured_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/featured"))?.into()) +pub fn generate_featured_url(ap_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{ap_id}/featured"))?.into()) } pub fn generate_moderators_url(community_id: &DbUrl) -> LemmyResult { @@ -1029,7 +1045,6 @@ fn limit_expire_time(expires: DateTime) -> LemmyResult } } -#[tracing::instrument(skip_all)] pub fn check_conflicting_like_filters( liked_only: Option, disliked_only: Option, @@ -1043,7 +1058,7 @@ pub fn check_conflicting_like_filters( pub async fn process_markdown( text: &str, - slur_regex: &Option>, + slur_regex: &Regex, url_blocklist: &RegexSet, context: &LemmyContext, ) -> LemmyResult { @@ -1075,7 +1090,7 @@ pub async fn process_markdown( pub async fn process_markdown_opt( text: &Option, - slur_regex: &Option>, + slur_regex: &Regex, url_blocklist: &RegexSet, context: &LemmyContext, ) -> LemmyResult> { @@ -1145,6 +1160,54 @@ fn build_proxied_image_url( )) } +pub async fn local_user_view_from_jwt( + jwt: &str, + context: &LemmyContext, +) -> LemmyResult { + let local_user_id = Claims::validate(jwt, context) + .await + .with_lemmy_type(LemmyErrorType::NotLoggedIn)?; + let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; + check_user_valid(&local_user_view.person)?; + + Ok(local_user_view) +} + +pub fn read_auth_token(req: &HttpRequest) -> LemmyResult> { + // Try reading jwt from auth header + if let Ok(header) = Authorization::::parse(req) { + Ok(Some(header.as_ref().token().to_string())) + } + // If that fails, try to read from cookie + else if let Some(cookie) = &req.cookie(AUTH_COOKIE_NAME) { + Ok(Some(cookie.value().to_string())) + } + // Otherwise, there's no auth + else { + Ok(None) + } +} + +pub fn send_webmention(post: Post, community: Community) { + if let Some(url) = post.url.clone() { + if community.visibility == CommunityVisibility::Public { + spawn_try_task(async move { + let mut webmention = Webmention::new::(post.ap_id.clone().into(), url.clone().into())?; + webmention.set_checked(true); + match webmention + .send() + .instrument(tracing::info_span!("Sending webmention")) + .await + { + Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()), + Ok(_) => Ok(()), + Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention), + } + }); + } + }; +} + #[cfg(test)] mod tests { @@ -1158,8 +1221,8 @@ mod tests { }, ModlogActionType, }; - use lemmy_db_views_moderator::{ - modlog_combined_view::ModlogCombinedQuery, + use lemmy_db_views::{ + combined::modlog_combined_view::ModlogCombinedQuery, structs::{ModRemoveCommentView, ModRemovePostView, ModlogCombinedView}, }; use pretty_assertions::assert_eq; diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index a05a4deed..012f20d4c 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -16,18 +16,15 @@ workspace = true lemmy_utils = { workspace = true, features = ["full"] } lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] } -lemmy_db_views_actor = { workspace = true, features = ["full"] } lemmy_api_common = { workspace = true, features = ["full"] } activitypub_federation = { workspace = true } bcrypt = { workspace = true } actix-web = { workspace = true } -tracing = { workspace = true } url = { workspace = true } futures.workspace = true uuid = { workspace = true } anyhow.workspace = true chrono.workspace = true -webmention = "0.6.0" accept-language = "3.1.0" regex = { workspace = true } serde_json = { workspace = true } diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 692b85c17..f85b5658c 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -10,8 +10,8 @@ use lemmy_api_common::{ check_post_deleted_or_removed, get_url_blocklist, is_mod_or_admin, - local_site_to_slur_regex, process_markdown, + slur_regex, update_read_comments, }, }; @@ -21,7 +21,6 @@ use lemmy_db_schema::{ source::{ comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, comment_reply::{CommentReply, CommentReplyUpdateForm}, - local_site::LocalSite, person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm}, }, traits::{Crud, Likeable}, @@ -33,15 +32,12 @@ use lemmy_utils::{ MAX_COMMENT_DEPTH_LIMIT, }; -#[tracing::instrument(skip(context))] pub async fn create_comment( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; - - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; is_valid_body_field(&content, false)?; @@ -146,7 +142,7 @@ pub async fn create_comment( update_read_comments( local_user_view.person.id, post_id, - post_view.counts.comments + 1, + post.comments + 1, &mut context.pool(), ) .await?; diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index cf90df6b6..41651cb09 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -15,7 +15,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn delete_comment( data: Json, context: Data, diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs index 39852081f..61abe0484 100644 --- a/crates/api_crud/src/comment/read.rs +++ b/crates/api_crud/src/comment/read.rs @@ -9,7 +9,6 @@ use lemmy_db_schema::source::local_site::LocalSite; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn get_comment( data: Query, context: Data, diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 4436f8c87..23160bb12 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -20,7 +20,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn remove_comment( data: Json, context: Data, diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 3cb1a3a4e..1ed588c81 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -6,20 +6,12 @@ use lemmy_api_common::{ comment::{CommentResponse, EditComment}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{ - check_community_user_action, - get_url_blocklist, - local_site_to_slur_regex, - process_markdown_opt, - }, + utils::{check_community_user_action, get_url_blocklist, process_markdown_opt, slur_regex}, }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, newtypes::PostOrCommentId, - source::{ - comment::{Comment, CommentUpdateForm}, - local_site::LocalSite, - }, + source::comment::{Comment, CommentUpdateForm}, traits::Crud, }; use lemmy_db_views::structs::{CommentView, LocalUserView}; @@ -28,14 +20,11 @@ use lemmy_utils::{ utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, }; -#[tracing::instrument(skip(context))] pub async fn update_comment( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; - let comment_id = data.comment_id; let orig_comment = CommentView::read( &mut context.pool(), @@ -64,7 +53,7 @@ pub async fn update_comment( ) .await?; - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown_opt(&data.content, &slur_regex, &url_blocklist, &context).await?; if let Some(content) = &content { diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index 0437396cc..df8e2ac76 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -6,14 +6,13 @@ use lemmy_api_common::{ community::{CommunityResponse, CreateCommunity}, context::LemmyContext, utils::{ + check_nsfw_allowed, generate_followers_url, generate_inbox_url, - generate_local_apub_endpoint, get_url_blocklist, is_admin, - local_site_to_slur_regex, process_markdown_opt, - EndpointType, + slur_regex, }, }; use lemmy_db_schema::{ @@ -44,7 +43,6 @@ use lemmy_utils::{ }, }; -#[tracing::instrument(skip(context))] pub async fn create_community( data: Json, context: Data, @@ -57,7 +55,8 @@ pub async fn create_community( Err(LemmyErrorType::OnlyAdminsCanCreateCommunities)? } - let slur_regex = local_site_to_slur_regex(&local_site); + check_nsfw_allowed(data.nsfw, Some(&local_site))?; + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; check_slurs(&data.name, &slur_regex)?; check_slurs(&data.title, &slur_regex)?; @@ -83,13 +82,8 @@ pub async fn create_community( check_community_visibility_allowed(data.visibility, &local_user_view)?; // Double check for duplicate community actor_ids - let community_actor_id = generate_local_apub_endpoint( - EndpointType::Community, - &data.name, - &context.settings().get_protocol_and_hostname(), - )?; - let community_dupe = - Community::read_from_apub_id(&mut context.pool(), &community_actor_id).await?; + let community_ap_id = Community::local_url(&data.name, context.settings())?; + let community_dupe = Community::read_from_apub_id(&mut context.pool(), &community_ap_id).await?; if community_dupe.is_some() { Err(LemmyErrorType::CommunityAlreadyExists)? } @@ -101,9 +95,9 @@ pub async fn create_community( sidebar, description, nsfw: data.nsfw, - actor_id: Some(community_actor_id.clone()), + ap_id: Some(community_ap_id.clone()), private_key: Some(keypair.private_key), - followers_url: Some(generate_followers_url(&community_actor_id)?), + followers_url: Some(generate_followers_url(&community_ap_id)?), inbox_url: Some(generate_inbox_url()?), posting_restricted_to_mods: data.posting_restricted_to_mods, visibility: data.visibility, diff --git a/crates/api_crud/src/community/delete.rs b/crates/api_crud/src/community/delete.rs index 7f9d04933..30fa5c1e2 100644 --- a/crates/api_crud/src/community/delete.rs +++ b/crates/api_crud/src/community/delete.rs @@ -11,11 +11,9 @@ use lemmy_db_schema::{ source::community::{Community, CommunityUpdateForm}, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityModeratorView; +use lemmy_db_views::structs::{CommunityModeratorView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn delete_community( data: Json, context: Data, diff --git a/crates/api_crud/src/community/list.rs b/crates/api_crud/src/community/list.rs index 9c13ae89f..19d26ae04 100644 --- a/crates/api_crud/src/community/list.rs +++ b/crates/api_crud/src/community/list.rs @@ -4,11 +4,12 @@ use lemmy_api_common::{ context::LemmyContext, utils::{check_private_instance, is_admin}, }; -use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_db_views_actor::community_view::CommunityQuery; +use lemmy_db_views::{ + community::community_view::CommunityQuery, + structs::{LocalUserView, SiteView}, +}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn list_communities( data: Query, context: Data, @@ -23,15 +24,18 @@ pub async fn list_communities( check_private_instance(&local_user_view, &local_site.local_site)?; let sort = data.sort; + let time_range_seconds = data.time_range_seconds; let listing_type = data.type_; let show_nsfw = data.show_nsfw.unwrap_or_default(); let page = data.page; let limit = data.limit; let local_user = local_user_view.map(|l| l.local_user); + let communities = CommunityQuery { listing_type, show_nsfw, sort, + time_range_seconds, local_user: local_user.as_ref(), page, limit, diff --git a/crates/api_crud/src/community/remove.rs b/crates/api_crud/src/community/remove.rs index 7dc78a37a..c379daa73 100644 --- a/crates/api_crud/src/community/remove.rs +++ b/crates/api_crud/src/community/remove.rs @@ -10,14 +10,14 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityUpdateForm}, + community_report::CommunityReport, mod_log::moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, }, - traits::Crud, + traits::{Crud, Reportable}, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn remove_community( data: Json, context: Data, @@ -49,6 +49,13 @@ pub async fn remove_community( .await .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunity)?; + CommunityReport::resolve_all_for_object( + &mut context.pool(), + community_id, + local_user_view.person.id, + ) + .await?; + // Mod tables let form = ModRemoveCommunityForm { mod_person_id: local_user_view.person.id, diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index 944f5bade..61bd21936 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -9,9 +9,10 @@ use lemmy_api_common::{ send_activity::{ActivityChannel, SendActivityData}, utils::{ check_community_mod_action, + check_nsfw_allowed, get_url_blocklist, - local_site_to_slur_regex, process_markdown_opt, + slur_regex, }, }; use lemmy_db_schema::{ @@ -29,7 +30,6 @@ use lemmy_utils::{ utils::{slurs::check_slurs_opt, validation::is_valid_body_field}, }; -#[tracing::instrument(skip(context))] pub async fn update_community( data: Json, context: Data, @@ -37,9 +37,10 @@ pub async fn update_community( ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; check_slurs_opt(&data.title, &slur_regex)?; + check_nsfw_allowed(data.nsfw, Some(&local_site))?; let sidebar = diesel_string_update( process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context) diff --git a/crates/api_crud/src/custom_emoji/create.rs b/crates/api_crud/src/custom_emoji/create.rs index 333a7ce89..34668d379 100644 --- a/crates/api_crud/src/custom_emoji/create.rs +++ b/crates/api_crud/src/custom_emoji/create.rs @@ -15,7 +15,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn create_custom_emoji( data: Json, context: Data, diff --git a/crates/api_crud/src/custom_emoji/delete.rs b/crates/api_crud/src/custom_emoji/delete.rs index 818fd4d88..f7f27ced2 100644 --- a/crates/api_crud/src/custom_emoji/delete.rs +++ b/crates/api_crud/src/custom_emoji/delete.rs @@ -10,7 +10,6 @@ use lemmy_db_schema::{source::custom_emoji::CustomEmoji, traits::Crud}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn delete_custom_emoji( data: Json, context: Data, diff --git a/crates/api_crud/src/custom_emoji/list.rs b/crates/api_crud/src/custom_emoji/list.rs index 6ee5a44b0..e1d11a1fa 100644 --- a/crates/api_crud/src/custom_emoji/list.rs +++ b/crates/api_crud/src/custom_emoji/list.rs @@ -3,13 +3,11 @@ use lemmy_api_common::{ context::LemmyContext, custom_emoji::{ListCustomEmojis, ListCustomEmojisResponse}, }; -use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; +use lemmy_db_views::structs::CustomEmojiView; use lemmy_utils::error::LemmyError; -#[tracing::instrument(skip(context))] pub async fn list_custom_emojis( data: Query, - local_user_view: Option, context: Data, ) -> Result, LemmyError> { let custom_emojis = CustomEmojiView::list( diff --git a/crates/api_crud/src/custom_emoji/update.rs b/crates/api_crud/src/custom_emoji/update.rs index 6087f6969..af69565f6 100644 --- a/crates/api_crud/src/custom_emoji/update.rs +++ b/crates/api_crud/src/custom_emoji/update.rs @@ -15,7 +15,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn update_custom_emoji( data: Json, context: Data, diff --git a/crates/api_crud/src/oauth_provider/create.rs b/crates/api_crud/src/oauth_provider/create.rs index c1e30066a..0fbbbb833 100644 --- a/crates/api_crud/src/oauth_provider/create.rs +++ b/crates/api_crud/src/oauth_provider/create.rs @@ -13,7 +13,6 @@ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; use url::Url; -#[tracing::instrument(skip(context))] pub async fn create_oauth_provider( data: Json, context: Data, diff --git a/crates/api_crud/src/oauth_provider/delete.rs b/crates/api_crud/src/oauth_provider/delete.rs index 0d4d616cc..7780d77ff 100644 --- a/crates/api_crud/src/oauth_provider/delete.rs +++ b/crates/api_crud/src/oauth_provider/delete.rs @@ -10,7 +10,6 @@ use lemmy_db_schema::{source::oauth_provider::OAuthProvider, traits::Crud}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; -#[tracing::instrument(skip(context))] pub async fn delete_oauth_provider( data: Json, context: Data, diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs index f8631a487..4c514df7f 100644 --- a/crates/api_crud/src/oauth_provider/update.rs +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -10,7 +10,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; -#[tracing::instrument(skip(context))] pub async fn update_oauth_provider( data: Json, context: Data, diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 639ac57e5..75827b1b4 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -9,10 +9,12 @@ use lemmy_api_common::{ send_activity::SendActivityData, utils::{ check_community_user_action, + check_nsfw_allowed, get_url_blocklist, honeypot_check, - local_site_to_slur_regex, process_markdown_opt, + send_webmention, + slur_regex, }, }; use lemmy_db_schema::{ @@ -25,13 +27,10 @@ use lemmy_db_schema::{ }, traits::{Crud, Likeable}, utils::diesel_url_create, - CommunityVisibility, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityModeratorView; +use lemmy_db_views::structs::{CommunityModeratorView, LocalUserView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, - spawn_try_task, utils::{ mention::scrape_text_for_mentions, slurs::check_slurs, @@ -44,27 +43,23 @@ use lemmy_utils::{ }, }, }; -use tracing::Instrument; -use url::Url; -use webmention::{Webmention, WebmentionError}; -#[tracing::instrument(skip(context))] pub async fn create_post( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { + honeypot_check(&data.honeypot)?; let local_site = LocalSite::read(&mut context.pool()).await?; - honeypot_check(&data.honeypot)?; - - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; check_slurs(&data.name, &slur_regex)?; let url_blocklist = get_url_blocklist(&context).await?; let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?; let url = diesel_url_create(data.url.as_deref())?; let custom_thumbnail = diesel_url_create(data.custom_thumbnail.as_deref())?; + check_nsfw_allowed(data.nsfw, Some(&local_site))?; is_valid_post_title(&data.name)?; @@ -170,23 +165,3 @@ pub async fn create_post( build_post_response(&context, community_id, local_user_view, post_id).await } - -pub fn send_webmention(post: Post, community: Community) { - if let Some(url) = post.url.clone() { - if community.visibility == CommunityVisibility::Public { - spawn_try_task(async move { - let mut webmention = Webmention::new::(post.ap_id.clone().into(), url.clone().into())?; - webmention.set_checked(true); - match webmention - .send() - .instrument(tracing::info_span!("Sending webmention")) - .await - { - Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()), - Ok(_) => Ok(()), - Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention), - } - }); - } - }; -} diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs index e54086911..dcece057a 100644 --- a/crates/api_crud/src/post/delete.rs +++ b/crates/api_crud/src/post/delete.rs @@ -17,7 +17,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn delete_post( data: Json, context: Data, diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 3b6ef9414..a2e5a7c18 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -12,13 +12,11 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::{ - post_view::PostQuery, - structs::{LocalUserView, PostView, SiteView}, + post::post_view::PostQuery, + structs::{CommunityView, LocalUserView, PostView, SiteView}, }; -use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn get_post( data: Query, context: Data, @@ -71,7 +69,7 @@ pub async fn get_post( update_read_comments( person_id, post_id, - post_view.counts.comments, + post_view.post.comments, &mut context.pool(), ) .await?; @@ -86,8 +84,6 @@ pub async fn get_post( ) .await?; - let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; - // Fetch the cross_posts let cross_posts = if let Some(url) = &post_view.post.url { let mut x_posts = PostQuery { @@ -110,7 +106,6 @@ pub async fn get_post( Ok(Json(GetPostResponse { post_view, community_view, - moderators, cross_posts, })) } diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index 95aa5fc56..0a1c18b26 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -20,7 +20,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn remove_post( data: Json, context: Data, diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index a93708b22..66501af31 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -1,4 +1,4 @@ -use super::{convert_published_time, create::send_webmention}; +use super::convert_published_time; use activitypub_federation::config::Data; use actix_web::web::Json; use chrono::Utc; @@ -10,9 +10,11 @@ use lemmy_api_common::{ send_activity::SendActivityData, utils::{ check_community_user_action, + check_nsfw_allowed, get_url_blocklist, - local_site_to_slur_regex, process_markdown_opt, + send_webmention, + slur_regex, }, }; use lemmy_db_schema::{ @@ -43,21 +45,19 @@ use lemmy_utils::{ }; use std::ops::Deref; -#[tracing::instrument(skip(context))] pub async fn update_post( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; - let url = diesel_url_update(data.url.as_deref())?; let custom_thumbnail = diesel_url_update(data.custom_thumbnail.as_deref())?; let url_blocklist = get_url_blocklist(&context).await?; - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let body = diesel_string_update( process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context) @@ -65,6 +65,8 @@ pub async fn update_post( .as_deref(), ); + check_nsfw_allowed(data.nsfw, Some(&local_site))?; + let alt_text = diesel_string_update(data.alt_text.as_deref()); if let Some(name) = &data.name { diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index fd95a2b9e..b8f50d49f 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -6,37 +6,31 @@ use lemmy_api_common::{ send_activity::{ActivityChannel, SendActivityData}, utils::{ check_private_messages_enabled, - get_interface_language, get_url_blocklist, - local_site_to_slur_regex, process_markdown, send_email_to_user, + slur_regex, }, }; use lemmy_db_schema::{ source::{ - local_site::LocalSite, person_block::PersonBlock, private_message::{PrivateMessage, PrivateMessageInsertForm}, }, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PrivateMessageView; +use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{markdown::markdown_to_html, validation::is_valid_body_field}, }; -#[tracing::instrument(skip(context))] pub async fn create_private_message( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; - - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; is_valid_body_field(&content, false)?; @@ -74,7 +68,7 @@ pub async fn create_private_message( if view.recipient.local { let recipient_id = data.recipient_id; let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?; - let lang = get_interface_language(&local_recipient); + let lang = &local_recipient.local_user.interface_i18n_language(); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let sender_name = &local_user_view.person.name; let content = markdown_to_html(&content); diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index d06c8bc04..71642a0ab 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -9,11 +9,9 @@ use lemmy_db_schema::{ source::private_message::{PrivateMessage, PrivateMessageUpdateForm}, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PrivateMessageView; +use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn delete_private_message( data: Json, context: Data, diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 22c1da4a2..a9ea70b22 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -5,30 +5,23 @@ use lemmy_api_common::{ context::LemmyContext, private_message::{EditPrivateMessage, PrivateMessageResponse}, send_activity::{ActivityChannel, SendActivityData}, - utils::{get_url_blocklist, local_site_to_slur_regex, process_markdown}, + utils::{get_url_blocklist, process_markdown, slur_regex}, }; use lemmy_db_schema::{ - source::{ - local_site::LocalSite, - private_message::{PrivateMessage, PrivateMessageUpdateForm}, - }, + source::private_message::{PrivateMessage, PrivateMessageUpdateForm}, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PrivateMessageView; +use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::validation::is_valid_body_field, }; -#[tracing::instrument(skip(context))] pub async fn update_private_message( data: Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; - // Checking permissions let private_message_id = data.private_message_id; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; @@ -37,7 +30,7 @@ pub async fn update_private_message( } // Doing the update - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; is_valid_body_field(&content, false)?; diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 454b04dbd..2720fe537 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -11,8 +11,8 @@ use lemmy_api_common::{ get_url_blocklist, is_admin, local_site_rate_limit_to_rate_limit_config, - local_site_to_slur_regex, process_markdown_opt, + slur_regex, }, }; use lemmy_db_schema::{ @@ -41,7 +41,6 @@ use lemmy_utils::{ }; use url::Url; -#[tracing::instrument(skip(context))] pub async fn create_site( data: Json, context: Data, @@ -54,11 +53,11 @@ pub async fn create_site( validate_create_payload(&local_site, &data)?; - let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into(); + let ap_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into(); let inbox_url = Some(generate_inbox_url()?); let keypair = generate_actor_keypair()?; - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?; @@ -66,7 +65,7 @@ pub async fn create_site( name: Some(data.name.clone()), sidebar: diesel_string_update(sidebar.as_deref()), description: diesel_string_update(data.description.as_deref()), - actor_id: Some(actor_id), + ap_id: Some(ap_id), last_refreshed_at: Some(Utc::now()), inbox_url, private_key: Some(Some(keypair.private_key)), @@ -106,6 +105,7 @@ pub async fn create_site( comment_upvotes: data.comment_upvotes, comment_downvotes: data.comment_downvotes, disable_donation_dialog: data.disable_donation_dialog, + disallow_nsfw_content: data.disallow_nsfw_content, ..Default::default() }; @@ -150,11 +150,11 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> // Check that the slur regex compiles, and returns the regex if valid... // Prioritize using new slur regex from the request; if not provided, use the existing regex. let slur_regex = build_and_check_regex( - &create_site + create_site .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 64d2237a0..f9b3c3869 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -8,12 +8,10 @@ use lemmy_db_schema::source::{ oauth_provider::OAuthProvider, tagline::Tagline, }; -use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_db_views_actor::structs::PersonView; +use lemmy_db_views::structs::{LocalUserView, PersonView, SiteView}; use lemmy_utils::{build_cache, error::LemmyResult, CacheLock, VERSION}; use std::sync::LazyLock; -#[tracing::instrument(skip(context))] pub async fn get_site_v3( local_user_view: Option, context: Data, @@ -25,7 +23,6 @@ pub async fn get_site_v3( Ok(site) } -#[tracing::instrument(skip(context))] pub async fn get_site_v4( local_user_view: Option, context: Data, @@ -42,7 +39,7 @@ pub async fn get_site_v4( .map(|l| l.local_user.admin) .unwrap_or_default() { - site_response.admin_oauth_providers = None; + site_response.admin_oauth_providers = vec![]; } Ok(Json(site_response)) @@ -67,8 +64,8 @@ async fn read_site(context: &LemmyContext) -> LemmyResult { discussion_languages, blocked_urls, tagline, - oauth_providers: Some(oauth_providers), - admin_oauth_providers: Some(admin_oauth_providers), + oauth_providers, + admin_oauth_providers, image_upload_disabled: context.settings().pictrs()?.image_upload_disabled, }) } diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 439e9f9c4..81ba48798 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -10,8 +10,8 @@ use lemmy_api_common::{ get_url_blocklist, is_admin, local_site_rate_limit_to_rate_limit_config, - local_site_to_slur_regex, process_markdown_opt, + slur_regex, }, }; use lemmy_db_schema::{ @@ -24,7 +24,7 @@ use lemmy_db_schema::{ site::{Site, SiteUpdateForm}, }, traits::Crud, - utils::diesel_string_update, + utils::{diesel_opt_number_update, diesel_string_update}, RegistrationMode, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; @@ -43,7 +43,6 @@ use lemmy_utils::{ }, }; -#[tracing::instrument(skip(context))] pub async fn update_site( data: Json, context: Data, @@ -62,13 +61,15 @@ pub async fn update_site( SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?; } - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let sidebar = diesel_string_update( process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context) .await? .as_deref(), ); + let default_post_time_range_seconds = + diesel_opt_number_update(data.default_post_time_range_seconds); let site_form = SiteUpdateForm { name: data.name.clone(), @@ -94,6 +95,7 @@ pub async fn update_site( default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, default_post_sort_type: data.default_post_sort_type, + default_post_time_range_seconds, default_comment_sort_type: data.default_comment_sort_type, legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, @@ -112,6 +114,7 @@ pub async fn update_site( comment_upvotes: data.comment_upvotes, comment_downvotes: data.comment_downvotes, disable_donation_dialog: data.disable_donation_dialog, + disallow_nsfw_content: data.disallow_nsfw_content, ..Default::default() }; @@ -190,11 +193,11 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm // Check that the slur regex compiles, and return the regex if valid... // Prioritize using new slur regex from the request; if not provided, use the existing regex. let slur_regex = build_and_check_regex( - &edit_site + edit_site .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/create.rs b/crates/api_crud/src/tagline/create.rs index f67a26f68..52313bb70 100644 --- a/crates/api_crud/src/tagline/create.rs +++ b/crates/api_crud/src/tagline/create.rs @@ -3,19 +3,15 @@ use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, tagline::{CreateTagline, TaglineResponse}, - utils::{get_url_blocklist, is_admin, local_site_to_slur_regex, process_markdown}, + utils::{get_url_blocklist, is_admin, process_markdown, slur_regex}, }; use lemmy_db_schema::{ - source::{ - local_site::LocalSite, - tagline::{Tagline, TaglineInsertForm}, - }, + source::tagline::{Tagline, TaglineInsertForm}, traits::Crud, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; -#[tracing::instrument(skip(context))] pub async fn create_tagline( data: Json, context: Data, @@ -24,9 +20,7 @@ pub async fn create_tagline( // Make sure user is an admin is_admin(&local_user_view)?; - let local_site = LocalSite::read(&mut context.pool()).await?; - - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; diff --git a/crates/api_crud/src/tagline/delete.rs b/crates/api_crud/src/tagline/delete.rs index 9add3cfe6..24d51a6a2 100644 --- a/crates/api_crud/src/tagline/delete.rs +++ b/crates/api_crud/src/tagline/delete.rs @@ -10,7 +10,6 @@ use lemmy_db_schema::{source::tagline::Tagline, traits::Crud}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; -#[tracing::instrument(skip(context))] pub async fn delete_tagline( data: Json, context: Data, diff --git a/crates/api_crud/src/tagline/list.rs b/crates/api_crud/src/tagline/list.rs index 21929f547..e6b6ab18e 100644 --- a/crates/api_crud/src/tagline/list.rs +++ b/crates/api_crud/src/tagline/list.rs @@ -4,13 +4,10 @@ use lemmy_api_common::{ tagline::{ListTaglines, ListTaglinesResponse}, }; use lemmy_db_schema::source::tagline::Tagline; -use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; -#[tracing::instrument(skip(context))] pub async fn list_taglines( data: Query, - local_user_view: Option, context: Data, ) -> Result, LemmyError> { let taglines = Tagline::list(&mut context.pool(), data.page, data.limit).await?; diff --git a/crates/api_crud/src/tagline/update.rs b/crates/api_crud/src/tagline/update.rs index 30b7d4370..6a5c7cb99 100644 --- a/crates/api_crud/src/tagline/update.rs +++ b/crates/api_crud/src/tagline/update.rs @@ -4,19 +4,15 @@ use chrono::Utc; use lemmy_api_common::{ context::LemmyContext, tagline::{TaglineResponse, UpdateTagline}, - utils::{get_url_blocklist, is_admin, local_site_to_slur_regex, process_markdown}, + utils::{get_url_blocklist, is_admin, process_markdown, slur_regex}, }; use lemmy_db_schema::{ - source::{ - local_site::LocalSite, - tagline::{Tagline, TaglineUpdateForm}, - }, + source::tagline::{Tagline, TaglineUpdateForm}, traits::Crud, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; -#[tracing::instrument(skip(context))] pub async fn update_tagline( data: Json, context: Data, @@ -25,9 +21,7 @@ pub async fn update_tagline( // Make sure user is an admin is_admin(&local_user_view)?; - let local_site = LocalSite::read(&mut context.pool()).await?; - - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index e82ae8cc4..72779092d 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -10,17 +10,14 @@ use lemmy_api_common::{ check_registration_application, check_user_valid, generate_inbox_url, - generate_local_apub_endpoint, honeypot_check, - local_site_to_slur_regex, password_length_check, send_new_applicant_email_to_admins, - send_verification_email, - EndpointType, + send_verification_email_if_required, + slur_regex, }, }; use lemmy_db_schema::{ - aggregates::structs::PersonAggregates, newtypes::{InstanceId, OAuthProviderId}, source::{ actor_language::SiteLanguage, @@ -28,7 +25,6 @@ use lemmy_db_schema::{ language::Language, local_site::LocalSite, local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, oauth_account::{OAuthAccount, OAuthAccountInsertForm}, oauth_provider::OAuthProvider, person::{Person, PersonInsertForm}, @@ -104,7 +100,7 @@ pub async fn register( .await?; } - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; check_slurs(&data.username, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?; @@ -193,7 +189,6 @@ pub async fn register( Ok(Json(login_response)) } -#[tracing::instrument(skip(context))] pub async fn authenticate_with_oauth( data: Json, req: HttpRequest, @@ -331,7 +326,7 @@ pub async fn authenticate_with_oauth( .as_ref() .ok_or(LemmyErrorType::RegistrationUsernameRequired)?; - let slur_regex = local_site_to_slur_regex(&local_site); + let slur_regex = slur_regex(&context).await?; check_slurs(username, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?; @@ -404,7 +399,7 @@ pub async fn authenticate_with_oauth( login_response.jwt = Some(jwt); } - return Ok(Json(login_response)); + Ok(Json(login_response)) } async fn create_person( @@ -415,15 +410,11 @@ async fn create_person( ) -> Result { let actor_keypair = generate_actor_keypair()?; is_valid_actor_name(&username, local_site.actor_name_max_length as usize)?; - let actor_id = generate_local_apub_endpoint( - EndpointType::Person, - &username, - &context.settings().get_protocol_and_hostname(), - )?; + let ap_id = Person::local_url(&username, context.settings())?; // Register the new person let person_form = PersonInsertForm { - actor_id: Some(actor_id.clone()), + ap_id: Some(ap_id.clone()), inbox_url: Some(generate_inbox_url()?), private_key: Some(actor_keypair.private_key), ..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id) @@ -459,15 +450,19 @@ async fn create_local_user( // 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.site_id).await?; + + // Enable languages from `Accept-Language` header only if no site languages are set. Otherwise it + // is possible that browser languages are only set to e.g. French, and the user won't see any + // English posts. + if !discussion_languages.is_empty() { + for l in &language_tags { + if let Some(found) = all_languages.iter().find(|all| &all.code == l) { + language_ids.insert(found.id); + } + } + } language_ids.extend(discussion_languages); let language_ids = language_ids.into_iter().collect(); @@ -483,37 +478,6 @@ async fn create_local_user( Ok(inserted_local_user) } -async fn send_verification_email_if_required( - context: &Data, - local_site: &LocalSite, - local_user: &LocalUser, - person: &Person, -) -> LemmyResult { - let mut sent = false; - if !local_user.admin && local_site.require_email_verification && !local_user.email_verified { - let local_user_view = LocalUserView { - local_user: local_user.clone(), - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: person.clone(), - counts: PersonAggregates::default(), - }; - - send_verification_email( - &local_user_view, - &local_user - .email - .clone() - .ok_or(LemmyErrorType::EmailRequired)?, - &mut context.pool(), - context.settings(), - ) - .await?; - - sent = true; - } - Ok(sent) -} - fn validate_registration_answer( require_registration_application: bool, answer: &Option, diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 39598265a..e6557ffc9 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -16,7 +16,6 @@ use lemmy_db_schema::source::{ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn delete_account( data: Json, context: Data, diff --git a/crates/api_crud/src/user/my_user.rs b/crates/api_crud/src/user/my_user.rs index f7a92eb99..193f54c89 100644 --- a/crates/api_crud/src/user/my_user.rs +++ b/crates/api_crud/src/user/my_user.rs @@ -6,11 +6,9 @@ use lemmy_db_schema::source::{ instance_block::InstanceBlock, person_block::PersonBlock, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView}; +use lemmy_db_views::structs::{CommunityFollowerView, CommunityModeratorView, LocalUserView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn get_my_user( local_user_view: LocalUserView, context: Data, diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index bd6fe3d28..27a9a3d3c 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -21,7 +21,6 @@ workspace = true lemmy_utils = { workspace = true, features = ["full"] } lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] } -lemmy_db_views_actor = { workspace = true, features = ["full"] } lemmy_api_common = { workspace = true, features = ["full"] } activitypub_federation = { workspace = true } diesel = { workspace = true } @@ -36,16 +35,16 @@ url = { workspace = true } futures = { workspace = true } itertools = { workspace = true } uuid = { workspace = true } -async-trait = { workspace = true } +async-trait = "0.1.86" anyhow = { workspace = true } reqwest = { workspace = true } moka.workspace = true serde_with.workspace = true html2md = "0.2.15" -html2text = "0.12.6" +html2text = "0.14.0" stringreader = "0.1.1" enum_delegate = "0.2.0" -semver = "1.0.24" +semver = "1.0.25" [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/apub/assets/lemmy/activities/community/resolve_report_page.json b/crates/apub/assets/lemmy/activities/community/resolve_report_page.json new file mode 100644 index 000000000..6fc218c7d --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/resolve_report_page.json @@ -0,0 +1,14 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_user", + "to": ["http://enterprise.lemmy.ml/c/main"], + "type": "Resolve", + "id": "http://ds9.lemmy.ml/activities/flag/4323412-5e45-4a95-a15f-e0dc86361ba4", + "object": { + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": ["http://enterprise.lemmy.ml/c/main"], + "object": "http://enterprise.lemmy.ml/post/7", + "summary": "report this post", + "type": "Flag", + "id": "http://ds9.lemmy.ml/activities/flag/98b0933f-5e45-4a95-a15f-e0dc86361ba4" + } +} diff --git a/crates/apub/assets/peertube/activities/announce_video.json b/crates/apub/assets/peertube/activities/announce_video.json index afad09c0f..41cd64819 100644 --- a/crates/apub/assets/peertube/activities/announce_video.json +++ b/crates/apub/assets/peertube/activities/announce_video.json @@ -1,103 +1,15 @@ { - "type": "Announce", - "id": "https://framatube.org/videos/watch/60c4bea4-6bb2-4fce-8d9f-8a522575419d/announces/395533", - "actor": "https://framatube.org/video-channels/joinpeertube", - "object": "https://framatube.org/videos/watch/60c4bea4-6bb2-4fce-8d9f-8a522575419d", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://framatube.org/accounts/framasoft/followers"], "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { "RsaSignature2017": "https://w3id.org/security#RsaSignature2017" - }, - { - "pt": "https://joinpeertube.org/ns#", - "sc": "http://schema.org#", - "Hashtag": "as:Hashtag", - "uuid": "sc:identifier", - "category": "sc:category", - "licence": "sc:license", - "subtitleLanguage": "sc:subtitleLanguage", - "sensitive": "as:sensitive", - "language": "sc:inLanguage", - "isLiveBroadcast": "sc:isLiveBroadcast", - "liveSaveReplay": { - "@type": "sc:Boolean", - "@id": "pt:liveSaveReplay" - }, - "permanentLive": { - "@type": "sc:Boolean", - "@id": "pt:permanentLive" - }, - "Infohash": "pt:Infohash", - "Playlist": "pt:Playlist", - "PlaylistElement": "pt:PlaylistElement", - "originallyPublishedAt": "sc:datePublished", - "views": { - "@type": "sc:Number", - "@id": "pt:views" - }, - "state": { - "@type": "sc:Number", - "@id": "pt:state" - }, - "size": { - "@type": "sc:Number", - "@id": "pt:size" - }, - "fps": { - "@type": "sc:Number", - "@id": "pt:fps" - }, - "startTimestamp": { - "@type": "sc:Number", - "@id": "pt:startTimestamp" - }, - "stopTimestamp": { - "@type": "sc:Number", - "@id": "pt:stopTimestamp" - }, - "position": { - "@type": "sc:Number", - "@id": "pt:position" - }, - "commentsEnabled": { - "@type": "sc:Boolean", - "@id": "pt:commentsEnabled" - }, - "downloadEnabled": { - "@type": "sc:Boolean", - "@id": "pt:downloadEnabled" - }, - "waitTranscoding": { - "@type": "sc:Boolean", - "@id": "pt:waitTranscoding" - }, - "support": { - "@type": "sc:Text", - "@id": "pt:support" - }, - "likes": { - "@id": "as:likes", - "@type": "@id" - }, - "dislikes": { - "@id": "as:dislikes", - "@type": "@id" - }, - "playlists": { - "@id": "pt:playlists", - "@type": "@id" - }, - "shares": { - "@id": "as:shares", - "@type": "@id" - }, - "comments": { - "@id": "as:comments", - "@type": "@id" - } } - ] + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://tilvids.com/accounts/thelinuxexperiment/followers"], + "type": "Announce", + "id": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1/announces/299", + "actor": "https://tilvids.com/video-channels/thelinuxexperiment_channel", + "object": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1" } diff --git a/crates/apub/assets/peertube/objects/group.json b/crates/apub/assets/peertube/objects/group.json index 1817fb202..159bdcb0b 100644 --- a/crates/apub/assets/peertube/objects/group.json +++ b/crates/apub/assets/peertube/objects/group.json @@ -16,57 +16,55 @@ "@type": "sc:Text", "@id": "pt:support" }, - "icons": "as:icon" + "lemmy": "https://join-lemmy.org/ns#", + "postingRestrictedToMods": "lemmy:postingRestrictedToMods" } ], "type": "Group", - "id": "https://peertube.stream/video-channels/vu", - "following": "https://peertube.stream/video-channels/vu/following", - "followers": "https://peertube.stream/video-channels/vu/followers", - "playlists": "https://peertube.stream/video-channels/vu/playlists", - "inbox": "https://peertube.stream/video-channels/vu/inbox", - "outbox": "https://peertube.stream/video-channels/vu/outbox", - "preferredUsername": "vu", - "url": "https://peertube.stream/video-channels/vu", - "name": "VU", + "id": "https://tilvids.com/video-channels/thelinuxexperiment_channel", + "following": "https://tilvids.com/video-channels/thelinuxexperiment_channel/following", + "followers": "https://tilvids.com/video-channels/thelinuxexperiment_channel/followers", + "playlists": "https://tilvids.com/video-channels/thelinuxexperiment_channel/playlists", + "inbox": "https://tilvids.com/video-channels/thelinuxexperiment_channel/inbox", + "outbox": "https://tilvids.com/video-channels/thelinuxexperiment_channel/outbox", + "preferredUsername": "thelinuxexperiment_channel", + "url": "https://tilvids.com/video-channels/thelinuxexperiment_channel", + "name": "The Linux Experiment", "endpoints": { - "sharedInbox": "https://peertube.stream/inbox" + "sharedInbox": "https://tilvids.com/inbox" }, "publicKey": { - "id": "https://peertube.stream/video-channels/vu#main-key", - "owner": "https://peertube.stream/video-channels/vu", - "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtcWpN7efQx5C7ecWkw3r\nX4ViPy/bl3d3iyVLyP6z/3+WAUKJxqR+QKlNzxM7NglzB0B48NYu2cg4iuwKkSK9\ntrfMC/Ze0H10Wo/5kUH5YQKzLo4syHOuuM+1rbZFBbzVFwk4k0qqLFTXQ+Y6WNSS\nG9OlFYZNpRaUkgF8Q/KCsngn68qsZ0gLly9FJb+6+j3IppLJNXrBpFB5qulWibL+\neN+3XMnaTm6ge6X+rFti5r6dh10grL0KU/eZKmGyadgdwYdvR/LLtBWwFIwSJShk\nuIPhcz2zbkwrV3AixLe76TLGXX5M9qczfsVYLupyU7TwPlFM2ENDtDdfp41sWaZa\nxQIDAQAB\n-----END PUBLIC KEY-----" + "id": "https://tilvids.com/video-channels/thelinuxexperiment_channel#main-key", + "owner": "https://tilvids.com/video-channels/thelinuxexperiment_channel", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7mWF3Il0lE+nWiArDK4B\n8Z9rUCYR/C9651CcqPFIpHFLkJgoAkYxeMqfCo7lbXil1abaQERjgAYAJtdfObvY\neqUrHejEHAClFIO5BilyTP8b02RVZX6xxtTNF7jUEePFI0xOtPtt3Yz+YP0c6rz6\noyCCpqTy8LRfDkD9RATQrYfFxZCQ2yo2SlCoymNrDjoVwPI0XMZWHyMthKcaVwAq\ni+dYd0pmNUxdY9V042tIg+YwR3mOYvkXCNqy1SDygcIY6N5kdqioFoKxMK3MFApK\nY7tkfZkZXLlBdzHjjtYGHictaZzNYl4HV6onx//A21w0A7dGimlYd5bYLwz/BteD\nTwIDAQAB\n-----END PUBLIC KEY-----" }, - "published": "2020-12-10T16:07:08.406Z", + "published": "2020-06-30T13:45:17.984Z", "icon": [ { "type": "Image", "mediaType": "image/jpeg", "height": 48, "width": 48, - "url": "https://peertube.stream/lazy-static/avatars/45ec87d5-c8ec-4fcf-948f-d5a928b56496.jpg" + "url": "https://tilvids.com/lazy-static/avatars/1bbe97f1-d283-4db4-8bdd-e5320564aff9.jpg" }, { "type": "Image", "mediaType": "image/jpeg", "height": 120, "width": 120, - "url": "https://peertube.stream/lazy-static/avatars/3296c098-abbb-4fda-a67a-ab88e447ca19.jpg" + "url": "https://tilvids.com/lazy-static/avatars/13b0214b-edc0-4c5b-a04d-be648a3a370a.jpg" } ], - "image": { - "type": "Image", - "mediaType": "image/jpeg", - "height": 317, - "width": 1920, - "url": "https://peertube.stream/lazy-static/banners/550c0541-3021-4d4b-8654-54d0c4cda96d.jpg" - }, - "summary": "VU c'est du lundi au samedi sur France 5 à 20h00 \nRetrouvez les meilleurs moments de la télévision, en 6 minutes.\n\nChaîne PeerTube non-officielle.", - "support": "Suivre VU :\n- Twitter : https://twitter.com/vufrancetv\n- Facebook :https://www.facebook.com/vufrancetv/\n- Site : https://www.france.tv/france-5/vu/", - "attributedTo": [ + "image": [ { - "type": "Person", - "id": "https://peertube.stream/accounts/createurs" + "type": "Image", + "mediaType": "image/jpeg", + "height": 317, + "width": 1920, + "url": "https://tilvids.com/lazy-static/banners/1a8d6881-30c8-47cb-8576-7af62d869c45.jpg" } - ] + ], + "summary": "I'm Nick, and I like to tinker with Linux stuff. I'll bumble through distro reviews, tutorials, and general helpful tidbits and impressions onLinux desktop environments, applications, and news. \n\nYou might see a bit of Linux gaming here and there, and some more personal opinion pieces, but in the end, it's more or less all about Linux and FOSS !\n\nIf you want to stay up to snuff, follow me on Mastodon: https://mastodon.social/@thelinuxEXP \n\nIf you can, consider supporting the channel here: \nhttps://www.patreon.com/thelinuxexperiment", + "support": "Support the channel on Patreon: \nhttps://www.patreon.com/thelinuxexperiment\n\nSupport on Liberapay:\nhttps://liberapay.com/TheLinuxExperiment/", + "postingRestrictedToMods": true } diff --git a/crates/apub/assets/peertube/objects/person.json b/crates/apub/assets/peertube/objects/person.json index 2b1acdaad..6d3ecfd06 100644 --- a/crates/apub/assets/peertube/objects/person.json +++ b/crates/apub/assets/peertube/objects/person.json @@ -2,57 +2,48 @@ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", - { - "RsaSignature2017": "https://w3id.org/security#RsaSignature2017" - }, + { "RsaSignature2017": "https://w3id.org/security#RsaSignature2017" }, { "pt": "https://joinpeertube.org/ns#", "sc": "http://schema.org/", - "playlists": { - "@id": "pt:playlists", - "@type": "@id" - }, - "support": { - "@type": "sc:Text", - "@id": "pt:support" - }, - "icons": "as:icon" + "playlists": { "@id": "pt:playlists", "@type": "@id" }, + "support": { "@type": "sc:Text", "@id": "pt:support" }, + "lemmy": "https://join-lemmy.org/ns#", + "postingRestrictedToMods": "lemmy:postingRestrictedToMods" } ], "type": "Person", - "id": "https://peertube.stream/accounts/createurs", - "following": "https://peertube.stream/accounts/createurs/following", - "followers": "https://peertube.stream/accounts/createurs/followers", - "playlists": "https://peertube.stream/accounts/createurs/playlists", - "inbox": "https://peertube.stream/accounts/createurs/inbox", - "outbox": "https://peertube.stream/accounts/createurs/outbox", - "preferredUsername": "createurs", - "url": "https://peertube.stream/accounts/createurs", - "name": "Créateurs", - "endpoints": { - "sharedInbox": "https://peertube.stream/inbox" - }, + "id": "https://tilvids.com/accounts/thelinuxexperiment", + "following": "https://tilvids.com/accounts/thelinuxexperiment/following", + "followers": "https://tilvids.com/accounts/thelinuxexperiment/followers", + "playlists": "https://tilvids.com/accounts/thelinuxexperiment/playlists", + "inbox": "https://tilvids.com/accounts/thelinuxexperiment/inbox", + "outbox": "https://tilvids.com/accounts/thelinuxexperiment/outbox", + "preferredUsername": "thelinuxexperiment", + "url": "https://tilvids.com/accounts/thelinuxexperiment", + "name": "The Linux Experiment", + "endpoints": { "sharedInbox": "https://tilvids.com/inbox" }, "publicKey": { - "id": "https://peertube.stream/accounts/createurs#main-key", - "owner": "https://peertube.stream/accounts/createurs", - "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxqkQhbRYbA81+WTYjorR\n2lEMad3kYCnzDjGTLr4I92eanzFHxyELGnjzP6TpEvjOiB9NrCRrqU/iFPLdgrq2\nwIFcXPWdCq6Gcg7QLlaeMM0JoJmr0KTEhzg0XKCo96UsyTzaF4DISxqi8RyoyWeU\nEkgiOzlkdYTlouq3MlQH+p1PBAsNUQfIEUsU+l6k1vzbm8JRwlT+D1bNde4I/Lqs\n4uB5ru3zzInwZ2hz9+heiriNoGEBv74rZHYn966tZVX8iMGx2+m6okozEdEQbqCl\n0ekqDcd8P6CoFqqeeu8coh82OUtuFI/XsbetdWA55YQmSHyMiTsIwVbeoogIETbI\n4QIDAQAB\n-----END PUBLIC KEY-----" + "id": "https://tilvids.com/accounts/thelinuxexperiment#main-key", + "owner": "https://tilvids.com/accounts/thelinuxexperiment", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqbMvBSLhwEA3VXQ3TPgd\nDCeVpicrjGlk5tRg9OMBMY/xRhT4M3T8H2uYMUmIQJubUcooqAImWL7bYyXig0Ms\nby18vLyAgIR7V7ymvJbJxF2WZV33CC7Ad1yjqLlnhydcG+pWKWqkjP7SXzAy/EHo\n46OhDQK1+Q6FXfDrLAGEDRq5z+qTi5dh1hi/c9ZvI0+3PBg1IfAf5zLeo1AoydV7\nvISCm7kyClABwOW3OjPP86SbAlQL6STFOO3s6EdvvVifTkacC/gl8ad8TI8610Wa\n5wLsjdE8LIky9lLUsFYvVPrJ6v5havxCSmc6W1tkDicitpFylN2X914L36bn609M\n8QIDAQAB\n-----END PUBLIC KEY-----" }, - "published": "2020-11-11T17:12:37.243Z", + "published": "2020-06-30T13:45:17.950Z", "icon": [ { "type": "Image", - "mediaType": "image/png", + "mediaType": "image/jpeg", "height": 48, "width": 48, - "url": "https://peertube.stream/lazy-static/avatars/1760df9a-3c96-45fc-9342-c313a3bf2210.png" + "url": "https://tilvids.com/lazy-static/avatars/e74c2c6b-1f6b-4506-9d03-2cbba1635b20.jpg" }, { "type": "Image", - "mediaType": "image/png", + "mediaType": "image/jpeg", "height": 120, "width": 120, - "url": "https://peertube.stream/lazy-static/avatars/c27b672d-ad8f-498a-adbe-553af8da56f9.png" + "url": "https://tilvids.com/lazy-static/avatars/bdaa7218-ba3c-43ba-abd3-cfd081394c18.jpg" } ], - "summary": "Centralisation de miroirs de chaînes. La grande majorité a été contactée ou diffuse sous licence avec paternité.\n\nCompte maintenu par [Raph](https://tooter.social/@raph)." + "summary": "I'm Nick, and I like to tinker with Linux stuff. I'll bumble through distro reviews, tutorials, and general helpful tidbits and impressions on Linux desktop environments, applications, and news. \n\nYou might see a bit of Linux gaming here and there, and some more personal opinion pieces, but in the end, it's more or less all about Linux and FOSS !\n\nIf you want to stay up to snuff, follow me on Mastodon @TheLinuxEXP@mastodon.social" } diff --git a/crates/apub/assets/peertube/objects/video.json b/crates/apub/assets/peertube/objects/video.json index daca3d554..d5417a7e3 100644 --- a/crates/apub/assets/peertube/objects/video.json +++ b/crates/apub/assets/peertube/objects/video.json @@ -9,10 +9,10 @@ "pt": "https://joinpeertube.org/ns#", "sc": "http://schema.org/", "Hashtag": "as:Hashtag", - "uuid": "sc:identifier", "category": "sc:category", "licence": "sc:license", "subtitleLanguage": "sc:subtitleLanguage", + "automaticallyGenerated": "pt:automaticallyGenerated", "sensitive": "as:sensitive", "language": "sc:inLanguage", "identifier": "sc:identifier", @@ -42,6 +42,14 @@ "@type": "sc:Number", "@id": "pt:tileDuration" }, + "aspectRatio": { + "@type": "sc:Float", + "@id": "pt:aspectRatio" + }, + "uuid": { + "@type": "sc:identifier", + "@id": "pt:uuid" + }, "originallyPublishedAt": "sc:datePublished", "uploadDate": "sc:uploadDate", "hasParts": "sc:hasParts", @@ -65,6 +73,11 @@ "@type": "sc:Boolean", "@id": "pt:commentsEnabled" }, + "canReply": "pt:canReply", + "commentsPolicy": { + "@type": "sc:Number", + "@id": "pt:commentsPolicy" + }, "downloadEnabled": { "@type": "sc:Boolean", "@id": "pt:downloadEnabled" @@ -92,54 +105,57 @@ "comments": { "@id": "as:comments", "@type": "@id" - } + }, + "PropertyValue": "sc:PropertyValue", + "value": "sc:value" } ], "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://peertube.stream/accounts/createurs/followers"], + "cc": ["https://tilvids.com/accounts/thelinuxexperiment/followers"], "type": "Video", - "id": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60", - "name": "VU du 12/12/23 : Démission \"refrusée\"", - "duration": "PT383S", - "uuid": "46cc7342-fdd5-4583-ae16-2eeb340d3b60", + "id": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1", + "name": "Mesa, Wayland & X.org in trouble, Debian leaves X, Facebook blocks Linux: Linux & Open Source News", + "duration": "PT1145S", + "uuid": "e7946124-7b72-4ad7-9d22-844a84bb2de1", "category": { - "identifier": "11", - "name": "News & Politics" + "identifier": "15", + "name": "Science & Technology" }, - "views": 83, + "licence": { + "identifier": "2", + "name": "Attribution - Share Alike" + }, + "language": { + "identifier": "en", + "name": "English" + }, + "views": 360, "sensitive": false, "waitTranscoding": true, "state": 1, "commentsEnabled": true, + "canReply": null, + "commentsPolicy": 1, "downloadEnabled": true, - "published": "2023-12-12T17:02:02.188Z", - "originallyPublishedAt": "2023-12-11T23:00:00.000Z", - "updated": "2023-12-14T06:40:34.279Z", - "tag": [ - { - "type": "Hashtag", - "name": "France3" - }, - { - "type": "Hashtag", - "name": "lezapping" - } - ], + "published": "2025-02-01T11:59:45.094Z", + "originallyPublishedAt": "2025-02-01T11:39:50.000Z", + "updated": "2025-02-04T09:00:50.396Z", + "tag": [], "mediaType": "text/markdown", - "content": "Un regard impertinent et libre, orchestré par Patrick Menais et son équipe, sur le monde de l’image.\n\nEn avant-première du lundi au samedi à17h00 sur Facebook, Twitter et YouTube.\n\nDu lundi au samedi à 20h00 sur France 5.\n\nhttps://www.facebook.com/vufrancetv\nhttps://twitter.com/VuFrancetv", - "support": null, + "content": "Head to https://squarespace.com/thelinuxexperiment to save 10% off your first purchase of a website or domain using code thelinuxexperiment\r\n\r\nGrab a brand new laptop or desktop running Linux: https://www.tuxedocomputers.com/en# \r\n\r\n\r\n👏 SUPPORT THE CHANNEL:\r\nGet access to:\r\n- a Daily Linux News show\r\n- a weekly patroncast for more personal thoughts\r\n- polls on the next topics I cover,\r\n- your name in the credits\r\n\r\nYouTube: https://www.youtube.com/@thelinuxexp/join\r\nPatreon: https://www.patreon.com/thelinuxexperiment\r\n\r\nOr, you can donate whatever you want:\r\nhttps://paypal.me/thelinuxexp\r\nLiberapay: https://liberapay.com/TheLinuxExperiment/\r\n\r\n👕 GET TLE MERCH\r\nSupport the channel AND get cool new gear: https://the-linux-experiment.creator-spring.com/\r\n\r\n🏆 FOLLOW ME ON THE FEDIVERSE:\r\nMastodon: https://mastodon.social/web/@thelinuxEXP\r\nPixelfed: https://pixelfed.social/TLENick\r\nPeerTube: https://tilvids.com/c/thelinuxexperiment_channel/videos\r\n\r\n🎙 LINUX AND OPEN SOURCE NEWS PODCAST:\r\nListen to the latestLinux and open source news, with more in depth coverage, and ad-free! https://podcast.thelinuxexp.com\r\n\r\nTimestamps:\r\n00:00 Intro\r\n00:34 Sponsor: Squarespace\r\n01:42 Mesa, Wayland and X.org lose their hosting\r\n03:57 Debian quits Twitter\r\n06:07 GNOME 48 alpha is out\r\n08:14 Kernel wifi maintainersteps down without replacement\r\n10:15 Facebook blocked posts linked to Linux\r\n12:12 OpenAI accuses another model of stealing their stolen work\r\n14:13Steam Deck is getting outclassed\r\n17:15 Sponsor: Tuxedo Computers\r\n18:10 Support the channel\r\n\r\nLinks:\r\n\r\nMesa, Wayland and X.org lose their hosting\r\nhttps://www.phoronix.com/news/2025-XOrg-FreeDesktop-Cloud\r\n\r\nDebian quits Twitter\r\nhttps://news.itsfoss.com/debian-logs-off-twitter/\r\n\r\nGNOME 48 alpha is out\r\nhttps://discourse.gnome.org/t/gnome-48-alpha-released/26414\r\nhttps://download.gnome.org/teams/releng/48.alpha.8/NEWS\r\n\r\nKernelwifi maintainer steps down without replacement\r\nhttps://linuxiac.com/linux-kernel-surpasses-40-million-lines/\r\nhttps://www.phoronix.com/news/Linux-Wireless-Maintainer-2025\r\n\r\nFacebook blocking posts linked to Linux\r\nhttps://distrowatch.com/weekly.php?issue=20250127#sitenews\r\nhttps://www.theregister.com/2025/01/28/facebook_blocks_distrowatch/\r\n\r\nOpenAI accuses another model of stealing their stolen work\r\nhttps://www.techradar.com/pro/us-navy-bans-use-of-deepseek-in-any-capacity-due-to-potential-security-and-ethical-concerns\r\nhttps://www.techradar.com/computing/artificial-intelligence/openai-says-deepseek-used-its-models-illegally-and-it-has-evidence-to-prove-it-new-report-claims\r\n\r\nSteam Deck is getting outclassed\r\nhttps://www.forbes.com/sites/jasonevangelho/2025/01/28/the-steam-deck-suddenly-has-a-serious-switch-2-problem/", + "support": "Support the channel on Patreon: \r\nhttps://www.patreon.com/thelinuxexperiment\r\n\r\nSupport on Liberapay:\r\nhttps://liberapay.com/TheLinuxExperiment/", "subtitleLanguage": [], "icon": [ { "type": "Image", - "url": "https://peertube.stream/lazy-static/thumbnails/208d2248-6fa3-4a58-a2e6-c6f176559457.jpg", + "url": "https://tilvids.com/lazy-static/thumbnails/904efceb-0715-476f-b0dc-b5fba6769851.jpg", "mediaType": "image/jpeg", "width": 280, "height": 157 }, { "type": "Image", - "url": "https://peertube.stream/lazy-static/previews/73d34e91-0233-443b-a1c3-d98a7ec6a87c.jpg", + "url": "https://tilvids.com/lazy-static/previews/ef6088ee-c83a-4fcf-8be2-58db95ca5135.jpg", "mediaType": "image/jpeg", "width": 850, "height": 480 @@ -152,256 +168,198 @@ "url": [ { "mediaType": "image/jpeg", - "href": "https://peertube.stream/lazy-static/storyboards/fb103d5f-8f76-4c8b-bc81-f952961cacfd.jpg", - "width": 1920, - "height": 1080, + "href": "https://tilvids.com/lazy-static/storyboards/b94d7ec4-97d4-4860-a4aa-220c5cf5beae.jpg", + "width": 2112, + "height": 1188, "tileWidth": 192, "tileHeight": 108, - "tileDuration": "PT4S" + "tileDuration": "PT10S" } ] } ], + "aspectRatio": 1.7778, "url": [ { "type": "Link", "mediaType": "text/html", - "href": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60" + "href": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1" }, { "type": "Link", "mediaType": "application/x-mpegURL", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/7847c00b-17f0-4cd9-b788-94283bd96d5b-master.m3u8", + "href": "https://tilvids.com/static/streaming-playlists/hls/e7946124-7b72-4ad7-9d22-844a84bb2de1/2020efb9-9f43-4e37-b268-3470a4bb89cd-master.m3u8", "tag": [ { "type": "Infohash", - "name": "f50d9a3e851756a1fc1da7fe8b6e40f849c1f3a1" + "name": "bade027756842ecef7a1fb7b437dcaa52eb72350" }, { "type": "Infohash", - "name": "fdddadfcf01c52808a5716ac9c0f09e379a1ca69" + "name": "dc1091029454a93ae893b207cfb1e7faf8d4d8b8" }, { "type": "Infohash", - "name": "c309597f071c6ab59e1a6935be3dc1ceb58c9250" - }, - { - "type": "Infohash", - "name": "5c28ed3e05102a678dc047a126650fe53d45ded4" - }, - { - "type": "Infohash", - "name": "085f2c72c69af02913177534ec601349ca2b4f01" - }, - { - "type": "Infohash", - "name": "37b9dbeab6f433e94f80a614f888e9a1e9ee3534" - }, - { - "type": "Infohash", - "name": "cc15513891e63a92743730ba65ab256f8825f071" + "name": "c83b5123b8dcb1b81b53fbdb4c95903cf61a2022" }, { "type": "Link", "name": "sha256", "mediaType": "application/json", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/a3f5af94-ba6b-4349-a4b0-151cebdf9af6-segments-sha256.json" + "href": "https://tilvids.com/static/streaming-playlists/hls/e7946124-7b72-4ad7-9d22-844a84bb2de1/0c0d34b1-ab46-4fc8-ae02-c97c23bfb2db-segments-sha256.json" }, { "type": "Link", "mediaType": "video/mp4", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/5a3db28f-a4b2-49ae-963e-7fd9414efe7c-1080-fragmented.mp4", + "href": "https://tilvids.com/static/streaming-playlists/hls/e7946124-7b72-4ad7-9d22-844a84bb2de1/0b870685-4461-47a3-8fac-e5531cd8acf5-1080-fragmented.mp4", "height": 1080, - "size": 90186372, - "fps": 25 + "width": 1920, + "size": 245864545, + "fps": 60, + "attachment": [ + { + "type": "PropertyValue", + "name": "ffprobe_codec_type", + "value": "audio" + }, + { + "type": "PropertyValue", + "name": "ffprobe_codec_type", + "value": "video" + }, + { + "type": "PropertyValue", + "name": "peertube_format_flag", + "value": "fragmented" + } + ] }, { "type": "Link", "rel": ["metadata", "video/mp4"], "mediaType": "application/json", - "href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570438", + "href": "https://tilvids.com/api/v1/videos/e7946124-7b72-4ad7-9d22-844a84bb2de1/metadata/729362", "height": 1080, - "fps": 25 + "width": 1920, + "fps": 60 }, { "type": "Link", "mediaType": "application/x-bittorrent", - "href": "https://peertube.stream/lazy-static/torrents/c3dd78f2-ff9b-41f1-899d-55440f512e09-1080-hls.torrent", - "height": 1080 + "href": "https://tilvids.com/lazy-static/torrents/cf3222e4-b9fe-4cb3-8b43-2da8afd83895-1080-hls.torrent", + "height": 1080, + "width": 1920, + "fps": 60 }, { "type": "Link", "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", - "href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2Fc3dd78f2-ff9b-41f1-899d-55440f512e09-1080-hls.torrent&xt=urn:btih:944323d8a38e077cdea5c1b1aa82300d1f49076a&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F5a3db28f-a4b2-49ae-963e-7fd9414efe7c-1080-fragmented.mp4", - "height": 1080 + "href": "magnet:?xs=https%3A%2F%2Ftilvids.com%2Flazy-static%2Ftorrents%2Fcf3222e4-b9fe-4cb3-8b43-2da8afd83895-1080-hls.torrent&xt=urn:btih:f9b4ddffa454ad6a7d5d7000d307c33f84aba1d1&dn=Mesa%2C+Wayland+%26+X.org+in+trouble%2C+Debian+leaves+X%2C+Facebook+blocks+Linux%3A+Linux+%26+Open+Source+News&tr=https%3A%2F%2Ftilvids.com%2Ftracker%2Fannounce&tr=wss%3A%2F%2Ftilvids.com%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Ftilvids.com%2Fstatic%2Fstreaming-playlists%2Fhls%2Fe7946124-7b72-4ad7-9d22-844a84bb2de1%2F0b870685-4461-47a3-8fac-e5531cd8acf5-1080-fragmented.mp4", + "height": 1080, + "width": 1920, + "fps": 60 }, { "type": "Link", "mediaType": "video/mp4", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/557f45f0-60b7-418c-bddd-e55701b387bb-720-fragmented.mp4", - "height": 720, - "size": 50950797, - "fps": 25 - }, - { - "type": "Link", - "rel": ["metadata", "video/mp4"], - "mediaType": "application/json", - "href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570447", - "height": 720, - "fps": 25 - }, - { - "type": "Link", - "mediaType": "application/x-bittorrent", - "href": "https://peertube.stream/lazy-static/torrents/0529c736-0c49-4efd-a9ff-c4989b4c2071-720-hls.torrent", - "height": 720 - }, - { - "type": "Link", - "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", - "href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F0529c736-0c49-4efd-a9ff-c4989b4c2071-720-hls.torrent&xt=urn:btih:a2662d0714edf3882193f782814441eb904460be&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F557f45f0-60b7-418c-bddd-e55701b387bb-720-fragmented.mp4", - "height": 720 - }, - { - "type": "Link", - "mediaType": "video/mp4", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/097e6338-4c6e-4c21-8fed-7df0a245c9b3-480-fragmented.mp4", - "height": 480, - "size": 31542462, - "fps": 25 - }, - { - "type": "Link", - "rel": ["metadata", "video/mp4"], - "mediaType": "application/json", - "href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570441", - "height": 480, - "fps": 25 - }, - { - "type": "Link", - "mediaType": "application/x-bittorrent", - "href": "https://peertube.stream/lazy-static/torrents/56b47f85-b2de-44b1-9089-db13c8534e1c-480-hls.torrent", - "height": 480 - }, - { - "type": "Link", - "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", - "href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F56b47f85-b2de-44b1-9089-db13c8534e1c-480-hls.torrent&xt=urn:btih:9d1cc84a448ba531d2f5422a8910fd79580768ff&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F097e6338-4c6e-4c21-8fed-7df0a245c9b3-480-fragmented.mp4", - "height": 480 - }, - { - "type": "Link", - "mediaType": "video/mp4", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/b6db1f0c-0b6f-4f26-b811-d38631f4c42b-360-fragmented.mp4", + "href": "https://tilvids.com/static/streaming-playlists/hls/e7946124-7b72-4ad7-9d22-844a84bb2de1/339ea14b-0fb9-495b-870e-218a9a6c22f9-360-fragmented.mp4", "height": 360, - "size": 23389554, - "fps": 25 + "width": 640, + "size": 62546436, + "fps": 30, + "attachment": [ + { + "type": "PropertyValue", + "name": "ffprobe_codec_type", + "value": "audio" + }, + { + "type": "PropertyValue", + "name": "ffprobe_codec_type", + "value": "video" + }, + { + "type": "PropertyValue", + "name": "peertube_format_flag", + "value": "fragmented" + } + ] }, { "type": "Link", "rel": ["metadata", "video/mp4"], "mediaType": "application/json", - "href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570442", + "href": "https://tilvids.com/api/v1/videos/e7946124-7b72-4ad7-9d22-844a84bb2de1/metadata/729352", "height": 360, - "fps": 25 + "width": 640, + "fps": 30 }, { "type": "Link", "mediaType": "application/x-bittorrent", - "href": "https://peertube.stream/lazy-static/torrents/89df203a-586e-4d09-b645-21c321ae81c2-360-hls.torrent", - "height": 360 + "href": "https://tilvids.com/lazy-static/torrents/dbdbd47d-42e8-4544-bb78-ae7835312cab-360-hls.torrent", + "height": 360, + "width": 640, + "fps": 30 }, { "type": "Link", "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", - "href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F89df203a-586e-4d09-b645-21c321ae81c2-360-hls.torrent&xt=urn:btih:40dbe1b6fb96d87d0750b32b26fd52913f22c84e&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2Fb6db1f0c-0b6f-4f26-b811-d38631f4c42b-360-fragmented.mp4", - "height": 360 + "href": "magnet:?xs=https%3A%2F%2Ftilvids.com%2Flazy-static%2Ftorrents%2Fdbdbd47d-42e8-4544-bb78-ae7835312cab-360-hls.torrent&xt=urn:btih:913416ac02f6bbfe7bb46e0b19bfe2a4a48d40b8&dn=Mesa%2C+Wayland+%26+X.org+in+trouble%2C+Debian+leaves+X%2C+Facebook+blocks+Linux%3A+Linux+%26+Open+Source+News&tr=https%3A%2F%2Ftilvids.com%2Ftracker%2Fannounce&tr=wss%3A%2F%2Ftilvids.com%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Ftilvids.com%2Fstatic%2Fstreaming-playlists%2Fhls%2Fe7946124-7b72-4ad7-9d22-844a84bb2de1%2F339ea14b-0fb9-495b-870e-218a9a6c22f9-360-fragmented.mp4", + "height": 360, + "width": 640, + "fps": 30 }, { "type": "Link", "mediaType": "video/mp4", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/d0d23e04-a7b2-47f9-8072-94a06dc0c402-240-fragmented.mp4", - "height": 240, - "size": 16040535, - "fps": 25 - }, - { - "type": "Link", - "rel": ["metadata", "video/mp4"], - "mediaType": "application/json", - "href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570448", - "height": 240, - "fps": 25 - }, - { - "type": "Link", - "mediaType": "application/x-bittorrent", - "href": "https://peertube.stream/lazy-static/torrents/29c43d5c-b26f-404c-a286-7aff2e2bb139-240-hls.torrent", - "height": 240 - }, - { - "type": "Link", - "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", - "href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F29c43d5c-b26f-404c-a286-7aff2e2bb139-240-hls.torrent&xt=urn:btih:f3f102c22d48b8a0aec19be463d8f04fb3a3f499&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2Fd0d23e04-a7b2-47f9-8072-94a06dc0c402-240-fragmented.mp4", - "height": 240 - }, - { - "type": "Link", - "mediaType": "video/mp4", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/6f3b1939-67c4-45f0-bd93-2508721dda69-144-fragmented.mp4", + "href": "https://tilvids.com/static/streaming-playlists/hls/e7946124-7b72-4ad7-9d22-844a84bb2de1/15585bf4-ff07-4687-8c01-537922958877-144-fragmented.mp4", "height": 144, - "size": 10969421, - "fps": 25 + "width": 256, + "size": 31021375, + "fps": 30, + "attachment": [ + { + "type": "PropertyValue", + "name": "ffprobe_codec_type", + "value": "audio" + }, + { + "type": "PropertyValue", + "name": "ffprobe_codec_type", + "value": "video" + }, + { + "type": "PropertyValue", + "name": "peertube_format_flag", + "value": "fragmented" + } + ] }, { "type": "Link", "rel": ["metadata", "video/mp4"], "mediaType": "application/json", - "href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570449", + "href": "https://tilvids.com/api/v1/videos/e7946124-7b72-4ad7-9d22-844a84bb2de1/metadata/729356", "height": 144, - "fps": 25 + "width": 256, + "fps": 30 }, { "type": "Link", "mediaType": "application/x-bittorrent", - "href": "https://peertube.stream/lazy-static/torrents/e39095d9-8fa2-4543-a66f-b4b9d6165a4e-144-hls.torrent", - "height": 144 + "href": "https://tilvids.com/lazy-static/torrents/f8a4e994-7be7-46b5-b823-29e041baf687-144-hls.torrent", + "height": 144, + "width": 256, + "fps": 30 }, { "type": "Link", "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", - "href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2Fe39095d9-8fa2-4543-a66f-b4b9d6165a4e-144-hls.torrent&xt=urn:btih:8b263d7e814d611597a36dcd9655d959c86605a4&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F6f3b1939-67c4-45f0-bd93-2508721dda69-144-fragmented.mp4", - "height": 144 - }, - { - "type": "Link", - "mediaType": "video/mp4", - "href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/86ab6cca-46e5-4c6e-9c2c-8aef803b85f2-0-fragmented.mp4", - "height": 0, - "size": 6074306, - "fps": 0 - }, - { - "type": "Link", - "rel": ["metadata", "video/mp4"], - "mediaType": "application/json", - "href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570439", - "height": 0, - "fps": 0 - }, - { - "type": "Link", - "mediaType": "application/x-bittorrent", - "href": "https://peertube.stream/lazy-static/torrents/25ae194d-c3ec-412a-886f-3b0d02599ca7-0-hls.torrent", - "height": 0 - }, - { - "type": "Link", - "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", - "href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F25ae194d-c3ec-412a-886f-3b0d02599ca7-0-hls.torrent&xt=urn:btih:e4458f2445732a228e9a83e2ae53a103f5e1097e&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F86ab6cca-46e5-4c6e-9c2c-8aef803b85f2-0-fragmented.mp4", - "height": 0 + "href": "magnet:?xs=https%3A%2F%2Ftilvids.com%2Flazy-static%2Ftorrents%2Ff8a4e994-7be7-46b5-b823-29e041baf687-144-hls.torrent&xt=urn:btih:6594dbb8a43e77ae7565fcd5744019f630c97706&dn=Mesa%2C+Wayland+%26+X.org+in+trouble%2C+Debian+leaves+X%2C+Facebook+blocks+Linux%3A+Linux+%26+Open+Source+News&tr=https%3A%2F%2Ftilvids.com%2Ftracker%2Fannounce&tr=wss%3A%2F%2Ftilvids.com%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Ftilvids.com%2Fstatic%2Fstreaming-playlists%2Fhls%2Fe7946124-7b72-4ad7-9d22-844a84bb2de1%2F15585bf4-ff07-4687-8c01-537922958877-144-fragmented.mp4", + "height": 144, + "width": 256, + "fps": 30 } ] }, @@ -409,33 +367,32 @@ "type": "Link", "name": "tracker-http", "rel": ["tracker", "http"], - "href": "https://peertube.stream/tracker/announce" + "href": "https://tilvids.com/tracker/announce" }, { "type": "Link", "name": "tracker-websocket", "rel": ["tracker", "websocket"], - "href": "wss://peertube.stream:443/tracker/socket" + "href": "wss://tilvids.com:443/tracker/socket" } ], - "likes": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/likes", - "dislikes": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/dislikes", - "shares": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/announces", - "comments": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/comments", - "hasParts": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/chapters", + "likes": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1/likes", + "dislikes": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1/dislikes", + "shares": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1/announces", + "comments": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1/comments", + "hasParts": "https://tilvids.com/videos/watch/e7946124-7b72-4ad7-9d22-844a84bb2de1/chapters", "attributedTo": [ { "type": "Person", - "id": "https://peertube.stream/accounts/createurs" + "id": "https://tilvids.com/accounts/thelinuxexperiment" }, { "type": "Group", - "id": "https://peertube.stream/video-channels/vu" + "id": "https://tilvids.com/video-channels/thelinuxexperiment_channel" } ], "isLiveBroadcast": false, "liveSaveReplay": null, "permanentLive": null, - "latencyMode": null, - "peertubeLiveChat": false + "latencyMode": null } diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 258de713f..32f0efb34 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -30,16 +30,11 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, - community::{ - CommunityFollower, - CommunityFollowerForm, - CommunityPersonBan, - CommunityPersonBanForm, - }, + community::{CommunityPersonBan, CommunityPersonBanForm}, mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, person::{Person, PersonUpdateForm}, }, - traits::{Bannable, Crud, Followable}, + traits::{Bannable, Crud}, }; use lemmy_utils::error::{FederationError, LemmyError, LemmyResult}; use url::Url; @@ -72,7 +67,6 @@ impl BlockUser { }) } - #[tracing::instrument(skip_all)] pub async fn send( target: &SiteOrCommunity, user: &ApubPerson, @@ -120,7 +114,6 @@ impl ActivityHandler for BlockUser { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { match self.target.dereference(context).await? { SiteOrCommunity::Site(site) => { @@ -148,7 +141,6 @@ impl ActivityHandler for BlockUser { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let expires = self.end_time; @@ -191,11 +183,9 @@ impl ActivityHandler for BlockUser { }; CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form).await?; - // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm::new(community.id, blocked_person.id); - CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) - .await - .ok(); + // Dont unsubscribe the user so that we can receive a potential unban activity. + // If we unfollowed the community here, activities from the community would be rejected + // in [[can_accept_activity_in_community]] in case are no other local followers. if self.remove_data.unwrap_or(false) { remove_or_restore_user_data_in_community( diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs index 6638fa052..0d635e09d 100644 --- a/crates/apub/src/activities/block/mod.rs +++ b/crates/apub/src/activities/block/mod.rs @@ -49,7 +49,6 @@ impl Object for SiteOrCommunity { type Kind = InstanceOrGroup; type Error = LemmyError; - #[tracing::instrument(skip_all)] fn last_refreshed_at(&self) -> Option> { Some(match self { SiteOrCommunity::Site(i) => i.last_refreshed_at, @@ -57,7 +56,6 @@ impl Object for SiteOrCommunity { }) } - #[tracing::instrument(skip_all)] async fn read_from_id(object_id: Url, data: &Data) -> LemmyResult> where Self: Sized, @@ -85,7 +83,6 @@ impl Object for SiteOrCommunity { }) } - #[tracing::instrument(skip_all)] async fn verify( apub: &Self::Kind, expected_domain: &Url, @@ -97,7 +94,6 @@ impl Object for SiteOrCommunity { } } - #[tracing::instrument(skip_all)] async fn from_json(apub: Self::Kind, data: &Data) -> LemmyResult where Self: Sized, @@ -114,8 +110,8 @@ impl Object for SiteOrCommunity { impl SiteOrCommunity { fn id(&self) -> ObjectId { match self { - SiteOrCommunity::Site(s) => ObjectId::from(s.actor_id.clone()), - SiteOrCommunity::Community(c) => ObjectId::from(c.actor_id.clone()), + SiteOrCommunity::Site(s) => ObjectId::from(s.ap_id.clone()), + SiteOrCommunity::Community(c) => ObjectId::from(c.ap_id.clone()), } } } @@ -125,7 +121,7 @@ async fn generate_cc(target: &SiteOrCommunity, pool: &mut DbPool<'_>) -> LemmyRe SiteOrCommunity::Site(_) => Site::read_remote_sites(pool) .await? .into_iter() - .map(|s| s.actor_id.into()) + .map(|s| s.ap_id.into()) .collect(), SiteOrCommunity::Community(c) => vec![c.id()], }) diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index d4d9a7a6e..b8d5ab508 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -36,7 +36,6 @@ use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl UndoBlockUser { - #[tracing::instrument(skip_all)] pub async fn send( target: &SiteOrCommunity, user: &ApubPerson, @@ -89,14 +88,12 @@ impl ActivityHandler for UndoBlockUser { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { verify_domains_match(self.actor.inner(), self.object.actor.inner())?; self.object.verify(context).await?; Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let expires = self.object.end_time; diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index e63ea3b4e..d78ead935 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -44,12 +44,10 @@ impl ActivityHandler for RawAnnouncableActivities { &self.actor } - #[tracing::instrument(skip_all)] async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> Result<(), Self::Error> { let activity: AnnouncableActivities = self.clone().try_into()?; @@ -64,13 +62,13 @@ impl ActivityHandler for RawAnnouncableActivities { // verify and receive activity activity.verify(context).await?; - let actor_id = activity.actor().clone().into(); + let ap_id = activity.actor().clone().into(); activity.receive(context).await?; // if community is local, send activity to followers if let Some(community) = community { if community.local { - verify_person_in_community(&actor_id, &community, context).await?; + verify_person_in_community(&ap_id, &community, context).await?; AnnounceActivity::send(self, &community, context).await?; } } @@ -107,7 +105,6 @@ impl AnnounceActivity { }) } - #[tracing::instrument(skip_all)] pub async fn send( object: RawAnnouncableActivities, community: &ApubCommunity, @@ -154,12 +151,10 @@ impl ActivityHandler for AnnounceActivity { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, _context: &Data) -> LemmyResult<()> { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let object: AnnouncableActivities = self.object.object(context).await?.try_into()?; diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs index c84909ac1..8edfb3583 100644 --- a/crates/apub/src/activities/community/collection_add.rs +++ b/crates/apub/src/activities/community/collection_add.rs @@ -41,7 +41,6 @@ use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl CollectionAdd { - #[tracing::instrument(skip_all)] pub async fn send_add_mod( community: &ApubCommunity, added_mod: &ApubPerson, @@ -56,7 +55,7 @@ impl CollectionAdd { actor: actor.id().into(), to: generate_to(community)?, object: added_mod.id(), - target: generate_moderators_url(&community.actor_id)?.into(), + target: generate_moderators_url(&community.ap_id)?.into(), cc: vec![community.id()], kind: AddType::Add, id: id.clone(), @@ -81,7 +80,7 @@ impl CollectionAdd { actor: actor.id().into(), to: generate_to(community)?, object: featured_post.ap_id.clone().into(), - target: generate_featured_url(&community.actor_id)?.into(), + target: generate_featured_url(&community.ap_id)?.into(), cc: vec![community.id()], kind: AddType::Add, id: id.clone(), @@ -112,7 +111,6 @@ impl ActivityHandler for CollectionAdd { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { let community = self.community(context).await?; verify_visibility(&self.to, &self.cc, &community)?; @@ -121,7 +119,6 @@ impl ActivityHandler for CollectionAdd { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let (community, collection_type) = diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs index 3ffcca853..a5d5e4616 100644 --- a/crates/apub/src/activities/community/collection_remove.rs +++ b/crates/apub/src/activities/community/collection_remove.rs @@ -36,7 +36,6 @@ use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl CollectionRemove { - #[tracing::instrument(skip_all)] pub async fn send_remove_mod( community: &ApubCommunity, removed_mod: &ApubPerson, @@ -51,7 +50,7 @@ impl CollectionRemove { actor: actor.id().into(), to: generate_to(community)?, object: removed_mod.id(), - target: generate_moderators_url(&community.actor_id)?.into(), + target: generate_moderators_url(&community.ap_id)?.into(), id: id.clone(), cc: vec![community.id()], kind: RemoveType::Remove, @@ -76,7 +75,7 @@ impl CollectionRemove { actor: actor.id().into(), to: generate_to(community)?, object: featured_post.ap_id.clone().into(), - target: generate_featured_url(&community.actor_id)?.into(), + target: generate_featured_url(&community.ap_id)?.into(), cc: vec![community.id()], kind: RemoveType::Remove, id: id.clone(), @@ -107,7 +106,6 @@ impl ActivityHandler for CollectionRemove { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { let community = self.community(context).await?; verify_visibility(&self.to, &self.cc, &community)?; @@ -116,7 +114,6 @@ impl ActivityHandler for CollectionRemove { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let (community, collection_type) = diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs index 412711c6b..d22ef74d1 100644 --- a/crates/apub/src/activities/community/lock_page.rs +++ b/crates/apub/src/activities/community/lock_page.rs @@ -135,9 +135,9 @@ pub(crate) async fn send_lock_post( LockType::Lock, &context.settings().get_protocol_and_hostname(), )?; - let community_id = community.actor_id.inner().clone(); + let community_id = community.ap_id.inner().clone(); let lock = LockPage { - actor: actor.actor_id.clone().into(), + actor: actor.ap_id.clone().into(), to: generate_to(&community)?, object: ObjectId::from(post.ap_id), cc: vec![community_id.clone()], diff --git a/crates/apub/src/activities/community/mod.rs b/crates/apub/src/activities/community/mod.rs index 93c6e5c77..1efabe8e2 100644 --- a/crates/apub/src/activities/community/mod.rs +++ b/crates/apub/src/activities/community/mod.rs @@ -1,15 +1,22 @@ use crate::{ activities::send_lemmy_activity, activity_lists::AnnouncableActivities, - objects::{community::ApubCommunity, person::ApubPerson}, + fetcher::post_or_comment::PostOrComment, + objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson}, protocol::activities::community::announce::AnnounceActivity, }; -use activitypub_federation::{config::Data, traits::Actor}; +use activitypub_federation::{config::Data, fetch::object_id::ObjectId, traits::Actor}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ - source::{activity::ActivitySendTargets, person::PersonFollower}, + source::{ + activity::ActivitySendTargets, + person::{Person, PersonFollower}, + site::Site, + }, + traits::Crud, CommunityVisibility, }; +use lemmy_db_views::structs::CommunityModeratorView; use lemmy_utils::error::LemmyResult; pub mod announce; @@ -17,6 +24,7 @@ pub mod collection_add; pub mod collection_remove; pub mod lock_page; pub mod report; +pub mod resolve_report; pub mod update; /// This function sends all activities which are happening in a community to the right inboxes. @@ -70,3 +78,37 @@ pub(crate) async fn send_activity_in_community( send_lemmy_activity(context, activity.clone(), actor, inboxes, false).await?; Ok(()) } + +async fn report_inboxes( + object_id: ObjectId, + community: &ApubCommunity, + context: &Data, +) -> LemmyResult { + // send report to the community where object was posted + let mut inboxes = ActivitySendTargets::to_inbox(community.shared_inbox_or_inbox()); + + if community.local { + // send to all moderators + let moderators = + CommunityModeratorView::for_community(&mut context.pool(), community.id).await?; + for m in moderators { + inboxes.add_inbox(m.moderator.inbox_url.into()); + } + + // also send report to user's home instance if possible + let object_creator_id = match object_id.dereference_local(context).await? { + PostOrComment::Post(p) => p.creator_id, + PostOrComment::Comment(c) => c.creator_id, + }; + let object_creator = Person::read(&mut context.pool(), object_creator_id).await?; + let object_creator_site: Option = + Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id) + .await + .ok() + .map(Into::into); + if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) { + inboxes.add_inbox(inbox); + } + } + Ok(inboxes) +} diff --git a/crates/apub/src/activities/community/report.rs b/crates/apub/src/activities/community/report.rs index 804822d6e..f0eced013 100644 --- a/crates/apub/src/activities/community/report.rs +++ b/crates/apub/src/activities/community/report.rs @@ -1,9 +1,14 @@ +use super::report_inboxes; use crate::{ activities::{generate_activity_id, send_lemmy_activity, verify_person_in_community}, + activity_lists::AnnouncableActivities, insert_received_activity, - objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson}, + objects::{community::ApubCommunity, person::ApubPerson}, protocol::{ - activities::community::report::{Report, ReportObject}, + activities::community::{ + announce::AnnounceActivity, + report::{Report, ReportObject}, + }, InCommunity, }, PostOrComment, @@ -20,63 +25,49 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - activity::ActivitySendTargets, comment_report::{CommentReport, CommentReportForm}, - community::Community, - person::Person, post_report::{PostReport, PostReportForm}, - site::Site, }, - traits::{Crud, Reportable}, + traits::Reportable, }; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl Report { - #[tracing::instrument(skip_all)] - pub(crate) async fn send( - object_id: ObjectId, - actor: Person, - community: Community, - reason: String, - context: Data, - ) -> LemmyResult<()> { - let actor: ApubPerson = actor.into(); - let community: ApubCommunity = community.into(); + pub(crate) fn new( + object_id: &ObjectId, + actor: &ApubPerson, + community: &ApubCommunity, + reason: Option, + context: &Data, + ) -> LemmyResult { let kind = FlagType::Flag; let id = generate_activity_id( kind.clone(), &context.settings().get_protocol_and_hostname(), )?; - let report = Report { + Ok(Report { actor: actor.id().into(), to: [community.id().into()], object: ReportObject::Lemmy(object_id.clone()), - summary: Some(reason), + summary: reason, content: None, kind, id: id.clone(), - }; + }) + } - // send report to the community where object was posted - let mut inboxes = ActivitySendTargets::to_inbox(community.shared_inbox_or_inbox()); + pub(crate) async fn send( + object_id: ObjectId, + actor: &ApubPerson, + community: &ApubCommunity, + reason: String, + context: Data, + ) -> LemmyResult<()> { + let report = Self::new(&object_id, actor, community, Some(reason), &context)?; + let inboxes = report_inboxes(object_id, community, &context).await?; - // also send report to user's home instance if possible - let object_creator_id = match object_id.dereference_local(&context).await? { - PostOrComment::Post(p) => p.creator_id, - PostOrComment::Comment(c) => c.creator_id, - }; - let object_creator = Person::read(&mut context.pool(), object_creator_id).await?; - let object_creator_site: Option = - Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id) - .await - .ok() - .map(Into::into); - if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) { - inboxes.add_inbox(inbox); - } - - send_lemmy_activity(&context, report, &actor, inboxes, false).await + send_lemmy_activity(&context, report, actor, inboxes, false).await } } @@ -93,14 +84,12 @@ impl ActivityHandler for Report { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { let community = self.community(context).await?; verify_person_in_community(&self.actor, &community, context).await?; Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let actor = self.actor.dereference(context).await?; @@ -116,6 +105,7 @@ impl ActivityHandler for Report { original_post_url: post.url.clone(), reason, original_post_body: post.body.clone(), + violates_instance_rules: false, }; PostReport::report(&mut context.pool(), &report_form).await?; } @@ -127,10 +117,22 @@ impl ActivityHandler for Report { comment_id: comment.id, original_comment_text: comment.content.clone(), reason, + violates_instance_rules: false, }; CommentReport::report(&mut context.pool(), &report_form).await?; } }; + + let community = self.community(context).await?; + if community.local { + // forward to remote mods + let object_id = self.object.object_id(context).await?; + let announce = AnnouncableActivities::Report(self); + let announce = AnnounceActivity::new(announce.try_into()?, &community, context)?; + let inboxes = report_inboxes(object_id, &community, context).await?; + send_lemmy_activity(context, announce, &community, inboxes.clone(), false).await?; + } + Ok(()) } } diff --git a/crates/apub/src/activities/community/resolve_report.rs b/crates/apub/src/activities/community/resolve_report.rs new file mode 100644 index 000000000..e59e1fea6 --- /dev/null +++ b/crates/apub/src/activities/community/resolve_report.rs @@ -0,0 +1,110 @@ +use super::report_inboxes; +use crate::{ + activities::{ + generate_activity_id, + send_lemmy_activity, + verify_mod_action, + verify_person_in_community, + }, + activity_lists::AnnouncableActivities, + insert_received_activity, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::{ + activities::community::{ + announce::AnnounceActivity, + report::Report, + resolve_report::{ResolveReport, ResolveType}, + }, + InCommunity, + }, + PostOrComment, +}; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + protocol::verification::verify_urls_match, + traits::{ActivityHandler, Actor}, +}; +use lemmy_api_common::context::LemmyContext; +use lemmy_db_schema::{ + source::{comment_report::CommentReport, post_report::PostReport}, + traits::Reportable, +}; +use lemmy_utils::error::{LemmyError, LemmyResult}; +use url::Url; + +impl ResolveReport { + pub(crate) async fn send( + object_id: ObjectId, + actor: &ApubPerson, + report_creator: &ApubPerson, + community: &ApubCommunity, + context: Data, + ) -> LemmyResult<()> { + let kind = ResolveType::Resolve; + let id = generate_activity_id( + kind.clone(), + &context.settings().get_protocol_and_hostname(), + )?; + let object = Report::new(&object_id, report_creator, community, None, &context)?; + let resolve = ResolveReport { + actor: actor.id().into(), + to: [community.id().into()], + object, + kind, + id: id.clone(), + }; + let inboxes = report_inboxes(object_id, community, &context).await?; + + send_lemmy_activity(&context, resolve, actor, inboxes, false).await + } +} + +#[async_trait::async_trait] +impl ActivityHandler for ResolveReport { + type DataType = LemmyContext; + type Error = LemmyError; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + async fn verify(&self, context: &Data) -> LemmyResult<()> { + self.object.verify(context).await?; + let community = self.community(context).await?; + verify_person_in_community(&self.actor, &community, context).await?; + verify_urls_match(self.to[0].inner(), self.object.to[0].inner())?; + verify_mod_action(&self.actor, &community, context).await?; + Ok(()) + } + + async fn receive(self, context: &Data) -> LemmyResult<()> { + insert_received_activity(&self.id, context).await?; + let reporter = self.object.actor.dereference(context).await?; + let actor = self.actor.dereference(context).await?; + match self.object.object.dereference(context).await? { + PostOrComment::Post(post) => { + PostReport::resolve_apub(&mut context.pool(), post.id, reporter.id, actor.id).await?; + } + PostOrComment::Comment(comment) => { + CommentReport::resolve_apub(&mut context.pool(), comment.id, reporter.id, actor.id).await?; + } + }; + + let community = self.community(context).await?; + if community.local { + // forward to remote mods + let object_id = self.object.object.object_id(context).await?; + let announce = AnnouncableActivities::ResolveReport(self); + let announce = AnnounceActivity::new(announce.try_into()?, &community, context)?; + let inboxes = report_inboxes(object_id, &community, context).await?; + send_lemmy_activity(context, announce, &community, inboxes.clone(), false).await?; + } + + Ok(()) + } +} diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index 280038d5f..5de47089f 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -75,17 +75,15 @@ impl ActivityHandler for UpdateCommunity { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { let community = self.community(context).await?; verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; - ApubCommunity::verify(&self.object, &community.actor_id.clone().into(), context).await?; + ApubCommunity::verify(&self.object, &community.ap_id.clone().into(), context).await?; Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let community = self.community(context).await?; @@ -100,7 +98,7 @@ impl ActivityHandler for UpdateCommunity { published: self.object.published, updated: Some(self.object.updated), nsfw: Some(self.object.sensitive.unwrap_or(false)), - actor_id: Some(self.object.id.into()), + ap_id: Some(self.object.id.into()), public_key: Some(self.object.public_key.public_key_pem), last_refreshed_at: Some(Utc::now()), icon: Some(self.object.icon.map(|i| i.url.into())), diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index a363e60a6..4474d2476 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -28,7 +28,6 @@ use lemmy_api_common::{ utils::{check_post_deleted_or_removed, is_mod_or_admin}, }; use lemmy_db_schema::{ - aggregates::structs::CommentAggregates, newtypes::{PersonId, PostOrCommentId}, source::{ activity::ActivitySendTargets, @@ -47,7 +46,6 @@ use serde_json::{from_value, to_value}; use url::Url; impl CreateOrUpdateNote { - #[tracing::instrument(skip(comment, person_id, kind, context))] pub(crate) async fn send( comment: Comment, person_id: PersonId, @@ -120,7 +118,6 @@ impl ActivityHandler for CreateOrUpdateNote { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { let post = self.object.get_parents(context).await?.0; let community = self.community(context).await?; @@ -136,7 +133,6 @@ impl ActivityHandler for CreateOrUpdateNote { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; // Need to do this check here instead of Note::from_json because we need the person who @@ -163,7 +159,7 @@ impl ActivityHandler for CreateOrUpdateNote { CommentLike::like(&mut context.pool(), &like_form).await?; // Calculate initial hot_rank - CommentAggregates::update_hot_rank(&mut context.pool(), comment.id).await?; + Comment::update_hot_rank(&mut context.pool(), comment.id).await?; let do_send_email = self.kind == CreateOrUpdateType::Create; let actor = self.actor.dereference(context).await?; diff --git a/crates/apub/src/activities/create_or_update/note_wrapper.rs b/crates/apub/src/activities/create_or_update/note_wrapper.rs index ca79b45d2..5ca23eb03 100644 --- a/crates/apub/src/activities/create_or_update/note_wrapper.rs +++ b/crates/apub/src/activities/create_or_update/note_wrapper.rs @@ -31,13 +31,11 @@ impl ActivityHandler for CreateOrUpdateNoteWrapper { &self.actor } - #[tracing::instrument(skip_all)] async fn verify(&self, _context: &Data) -> LemmyResult<()> { // Do everything in receive to avoid extra checks. Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { // Use serde to convert NoteWrapper either into Comment or PrivateMessage, // depending on conditions below. This works because NoteWrapper keeps all @@ -63,7 +61,6 @@ impl ActivityHandler for CreateOrUpdateNoteWrapper { } } -#[async_trait::async_trait] impl InCommunity for CreateOrUpdateNoteWrapper { async fn community(&self, context: &Data) -> LemmyResult { // Same logic as in receive. In case this is a private message, an error is returned. diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs index 0aa0faf0b..d263c6f31 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -22,7 +22,6 @@ use activitypub_federation::{ }; use lemmy_api_common::{build_response::send_local_notifs, context::LemmyContext}; use lemmy_db_schema::{ - aggregates::structs::PostAggregates, newtypes::{PersonId, PostOrCommentId}, source::{ activity::ActivitySendTargets, @@ -60,7 +59,6 @@ impl CreateOrUpdatePage { }) } - #[tracing::instrument(skip_all)] pub(crate) async fn send( post: Post, person_id: PersonId, @@ -102,7 +100,6 @@ impl ActivityHandler for CreateOrUpdatePage { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { let community = self.community(context).await?; verify_visibility(&self.to, &self.cc, &community)?; @@ -114,7 +111,6 @@ impl ActivityHandler for CreateOrUpdatePage { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let post = ApubPost::from_json(self.object, context).await?; @@ -124,7 +120,7 @@ impl ActivityHandler for CreateOrUpdatePage { PostLike::like(&mut context.pool(), &like_form).await?; // Calculate initial hot_rank for post - PostAggregates::update_ranks(&mut context.pool(), post.id).await?; + Post::update_ranks(&mut context.pool(), post.id).await?; let do_send_email = self.kind == CreateOrUpdateType::Create; let actor = self.actor.dereference(context).await?; diff --git a/crates/apub/src/activities/create_or_update/private_message.rs b/crates/apub/src/activities/create_or_update/private_message.rs index ce04a9330..e5e6a699d 100644 --- a/crates/apub/src/activities/create_or_update/private_message.rs +++ b/crates/apub/src/activities/create_or_update/private_message.rs @@ -14,7 +14,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::source::activity::ActivitySendTargets; -use lemmy_db_views_actor::structs::PrivateMessageView; +use lemmy_db_views::structs::PrivateMessageView; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; @@ -56,7 +56,6 @@ impl ActivityHandler for CreateOrUpdatePrivateMessage { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { verify_person(&self.actor, context).await?; verify_domains_match(self.actor.inner(), self.object.id.inner())?; @@ -66,7 +65,6 @@ impl ActivityHandler for CreateOrUpdatePrivateMessage { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; ApubPrivateMessage::from_json(self.object, context).await?; diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index 48e24bbeb..935418693 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -14,6 +14,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, community::{Community, CommunityUpdateForm}, + community_report::CommunityReport, mod_log::moderator::{ ModRemoveComment, ModRemoveCommentForm, @@ -43,13 +44,11 @@ impl ActivityHandler for Delete { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { verify_delete_activity(self, self.summary.is_some(), context).await?; Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; if let Some(reason) = self.summary { @@ -93,9 +92,9 @@ impl Delete { DeleteType::Delete, &context.settings().get_protocol_and_hostname(), )?; - let cc: Option = community.map(|c| c.actor_id.clone().into()); + let cc: Option = community.map(|c| c.ap_id.clone().into()); Ok(Delete { - actor: actor.actor_id.clone().into(), + actor: actor.ap_id.clone().into(), to, object: IdOrNestedObject::Id(object.id()), cc: cc.into_iter().collect(), @@ -107,7 +106,6 @@ impl Delete { } } -#[tracing::instrument(skip_all)] pub(in crate::activities) async fn receive_remove_action( actor: &ApubPerson, object: &Url, @@ -119,6 +117,7 @@ pub(in crate::activities) async fn receive_remove_action( if community.local { Err(FederationError::OnlyLocalAdminCanRemoveCommunity)? } + CommunityReport::resolve_all_for_object(&mut context.pool(), community.id, actor.id).await?; let form = ModRemoveCommunityForm { mod_person_id: actor.id, community_id: community.id, diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs index 3b50cfd1a..c95d1086c 100644 --- a/crates/apub/src/activities/deletion/mod.rs +++ b/crates/apub/src/activities/deletion/mod.rs @@ -49,7 +49,6 @@ pub mod undo_delete; /// Parameter `reason` being set indicates that this is a removal by a mod. If its unset, this /// action was done by a normal user. -#[tracing::instrument(skip_all)] pub(crate) async fn send_apub_delete_in_community( actor: Person, community: Community, @@ -79,7 +78,6 @@ pub(crate) async fn send_apub_delete_in_community( .await } -#[tracing::instrument(skip_all)] pub(crate) async fn send_apub_delete_private_message( actor: &ApubPerson, pm: DbPrivateMessage, @@ -129,7 +127,6 @@ pub enum DeletableObjects { } impl DeletableObjects { - #[tracing::instrument(skip_all)] pub(crate) async fn read_from_db( ap_id: &Url, context: &Data, @@ -163,7 +160,6 @@ impl DeletableObjects { } } -#[tracing::instrument(skip_all)] pub(in crate::activities) async fn verify_delete_activity( activity: &Delete, is_mod_action: bool, @@ -184,7 +180,7 @@ pub(in crate::activities) async fn verify_delete_activity( DeletableObjects::Person(person) => { verify_is_public(&activity.to, &[])?; verify_person(&activity.actor, context).await?; - verify_urls_match(person.actor_id.inner(), activity.object.id())?; + verify_urls_match(person.ap_id.inner(), activity.object.id())?; } DeletableObjects::Post(p) => { let community = activity.community(context).await?; @@ -218,7 +214,6 @@ pub(in crate::activities) async fn verify_delete_activity( Ok(()) } -#[tracing::instrument(skip_all)] async fn verify_delete_post_or_comment( actor: &ObjectId, object_id: &Url, @@ -237,7 +232,6 @@ async fn verify_delete_post_or_comment( } /// Write deletion or restoring of an object to the database, and send websocket message. -#[tracing::instrument(skip_all)] async fn receive_delete_action( object: &Url, actor: &ObjectId, diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index 032936909..6badff6bc 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -47,7 +47,6 @@ impl ActivityHandler for UndoDelete { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; if self.object.summary.is_some() { @@ -64,7 +63,6 @@ impl ActivityHandler for UndoDelete { } impl UndoDelete { - #[tracing::instrument(skip_all)] pub(in crate::activities::deletion) fn new( actor: &ApubPerson, object: DeletableObjects, @@ -79,9 +77,9 @@ impl UndoDelete { UndoType::Undo, &context.settings().get_protocol_and_hostname(), )?; - let cc: Option = community.map(|c| c.actor_id.clone().into()); + let cc: Option = community.map(|c| c.ap_id.clone().into()); Ok(UndoDelete { - actor: actor.actor_id.clone().into(), + actor: actor.ap_id.clone().into(), to, object, cc: cc.into_iter().collect(), @@ -90,7 +88,6 @@ impl UndoDelete { }) } - #[tracing::instrument(skip_all)] pub(in crate::activities) async fn receive_undo_remove_action( actor: &ApubPerson, object: &Url, diff --git a/crates/apub/src/activities/following/accept.rs b/crates/apub/src/activities/following/accept.rs index fa711b904..eea279314 100644 --- a/crates/apub/src/activities/following/accept.rs +++ b/crates/apub/src/activities/following/accept.rs @@ -18,7 +18,6 @@ use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl AcceptFollow { - #[tracing::instrument(skip_all)] pub async fn send(follow: Follow, context: &Data) -> LemmyResult<()> { let user_or_community = follow.object.dereference_local(context).await?; let person = follow.actor.clone().dereference(context).await?; @@ -51,7 +50,6 @@ impl ActivityHandler for AcceptFollow { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { verify_urls_match(self.actor.inner(), self.object.object.inner())?; self.object.verify(context).await?; @@ -61,7 +59,6 @@ impl ActivityHandler for AcceptFollow { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let community = self.actor.dereference(context).await?; diff --git a/crates/apub/src/activities/following/follow.rs b/crates/apub/src/activities/following/follow.rs index befa2e00c..50bdcac61 100644 --- a/crates/apub/src/activities/following/follow.rs +++ b/crates/apub/src/activities/following/follow.rs @@ -21,12 +21,13 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{CommunityFollower, CommunityFollowerForm, CommunityFollowerState}, + instance::Instance, person::{PersonFollower, PersonFollowerForm}, }, traits::Followable, CommunityVisibility, }; -use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}; use url::Url; impl Follow { @@ -47,7 +48,6 @@ impl Follow { }) } - #[tracing::instrument(skip_all)] pub async fn send( actor: &ApubPerson, community: &ApubCommunity, @@ -76,7 +76,6 @@ impl ActivityHandler for Follow { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { verify_person(&self.actor, context).await?; let object = self.object.dereference(context).await?; @@ -89,7 +88,6 @@ impl ActivityHandler for Follow { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let actor = self.actor.dereference(context).await?; @@ -105,6 +103,13 @@ impl ActivityHandler for Follow { AcceptFollow::send(self, context).await?; } UserOrCommunity::Community(c) => { + if c.visibility == CommunityVisibility::Private { + let instance = Instance::read(&mut context.pool(), actor.instance_id).await?; + if [Some("kbin"), Some("mbin")].contains(&instance.software.as_deref()) { + // TODO: change this to a minimum version check once private communities are supported + return Err(FederationError::PlatformLackingPrivateCommunitySupport.into()); + } + } let state = Some(match c.visibility { CommunityVisibility::Public => CommunityFollowerState::Accepted, CommunityVisibility::Private => CommunityFollowerState::ApprovalRequired, diff --git a/crates/apub/src/activities/following/mod.rs b/crates/apub/src/activities/following/mod.rs index 83cdc841c..db4cb0143 100644 --- a/crates/apub/src/activities/following/mod.rs +++ b/crates/apub/src/activities/following/mod.rs @@ -47,9 +47,9 @@ pub async fn send_accept_or_reject_follow( let person = Person::read(&mut context.pool(), person_id).await?; let follow = Follow { - actor: person.actor_id.into(), - to: Some([community.actor_id.clone().into()]), - object: community.actor_id.into(), + actor: person.ap_id.into(), + to: Some([community.ap_id.clone().into()]), + object: community.ap_id.into(), kind: FollowType::Follow, id: generate_activity_id( FollowType::Follow, diff --git a/crates/apub/src/activities/following/reject.rs b/crates/apub/src/activities/following/reject.rs index 8f1623d20..d9b7ed547 100644 --- a/crates/apub/src/activities/following/reject.rs +++ b/crates/apub/src/activities/following/reject.rs @@ -21,7 +21,6 @@ use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl RejectFollow { - #[tracing::instrument(skip_all)] pub async fn send(follow: Follow, context: &Data) -> LemmyResult<()> { let user_or_community = follow.object.dereference_local(context).await?; let person = follow.actor.clone().dereference(context).await?; @@ -54,7 +53,6 @@ impl ActivityHandler for RejectFollow { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { verify_urls_match(self.actor.inner(), self.object.object.inner())?; self.object.verify(context).await?; @@ -64,7 +62,6 @@ impl ActivityHandler for RejectFollow { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let community = self.actor.dereference(context).await?; diff --git a/crates/apub/src/activities/following/undo_follow.rs b/crates/apub/src/activities/following/undo_follow.rs index 1aa6bb7fc..1688f133e 100644 --- a/crates/apub/src/activities/following/undo_follow.rs +++ b/crates/apub/src/activities/following/undo_follow.rs @@ -24,7 +24,6 @@ use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl UndoFollow { - #[tracing::instrument(skip_all)] pub async fn send( actor: &ApubPerson, community: &ApubCommunity, @@ -63,7 +62,6 @@ impl ActivityHandler for UndoFollow { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { verify_urls_match(self.actor.inner(), self.object.actor.inner())?; verify_person(&self.actor, context).await?; @@ -74,7 +72,6 @@ impl ActivityHandler for UndoFollow { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let person = self.actor.dereference(context).await?; diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index feef9cbd0..ea08ac956 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -18,7 +18,7 @@ use crate::{ }, objects::{community::ApubCommunity, person::ApubPerson}, protocol::activities::{ - community::report::Report, + community::{report::Report, resolve_report::ResolveReport}, create_or_update::{note::CreateOrUpdateNote, page::CreateOrUpdatePage}, CreateOrUpdateType, }, @@ -43,7 +43,7 @@ use lemmy_db_schema::{ traits::Crud, CommunityVisibility, }; -use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView}; +use lemmy_db_views::structs::{CommunityPersonBanView, CommunityView}; use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; use serde::Serialize; use tracing::info; @@ -59,7 +59,6 @@ pub mod voting; /// Checks that the specified Url actually identifies a Person (by fetching it), and that the person /// doesn't have a site ban. -#[tracing::instrument(skip_all)] async fn verify_person( person_id: &ObjectId, context: &Data, @@ -75,7 +74,6 @@ async fn verify_person( /// Fetches the person and community to verify their type, then checks if person is banned from site /// or community. -#[tracing::instrument(skip_all)] pub(crate) async fn verify_person_in_community( person_id: &ObjectId, community: &ApubCommunity, @@ -84,7 +82,7 @@ pub(crate) async fn verify_person_in_community( let person = person_id.dereference(context).await?; if person.banned { Err(FederationError::PersonIsBannedFromSite( - person.actor_id.to_string(), + person.ap_id.to_string(), ))? } let person_id = person.id; @@ -97,7 +95,6 @@ pub(crate) async fn verify_person_in_community( /// * `mod_id` - Activitypub ID of the mod or admin who performed the action /// * `object_id` - Activitypub ID of the actor or object that is being moderated /// * `community` - The community inside which moderation is happening -#[tracing::instrument(skip_all)] pub(crate) async fn verify_mod_action( mod_id: &ObjectId, community: &Community, @@ -106,7 +103,7 @@ pub(crate) async fn verify_mod_action( // mod action comes from the same instance as the community, so it was presumably done // by an instance admin. // TODO: federate instance admin status and check it here - if mod_id.inner().domain() == community.actor_id.domain() { + if mod_id.inner().domain() == community.ap_id.domain() { return Ok(()); } @@ -137,13 +134,13 @@ pub(crate) fn verify_visibility(to: &[Url], cc: &[Url], community: &Community) - /// Marks object as public only if the community is public pub(crate) fn generate_to(community: &Community) -> LemmyResult> { - let actor_id = community.actor_id.clone().into(); + let ap_id = community.ap_id.clone().into(); if community.visibility == CommunityVisibility::Public { - Ok(vec![actor_id, public()]) + Ok(vec![ap_id, public()]) } else { Ok(vec![ - actor_id.clone(), - Url::parse(&format!("{}/followers", actor_id))?, + ap_id.clone(), + Url::parse(&format!("{}/followers", ap_id))?, ]) } } @@ -190,7 +187,6 @@ pub(crate) trait GetActorType { fn actor_type(&self) -> ActorType; } -#[tracing::instrument(skip_all)] async fn send_lemmy_activity( data: &Data, activity: Activity, @@ -382,7 +378,31 @@ pub async fn match_outgoing_activities( actor, community, reason, - } => Report::send(ObjectId::from(object_id), actor, community, reason, context).await, + } => { + Report::send( + ObjectId::from(object_id), + &actor.into(), + &community.into(), + reason, + context, + ) + .await + } + SendResolveReport { + object_id, + actor, + report_creator, + community, + } => { + ResolveReport::send( + ObjectId::from(object_id), + &actor.into(), + &report_creator.into(), + &community.into(), + context, + ) + .await + } AcceptFollower(community_id, person_id) => { send_accept_or_reject_follow(community_id, person_id, true, &context).await } diff --git a/crates/apub/src/activities/voting/mod.rs b/crates/apub/src/activities/voting/mod.rs index 4ce0f695b..9427b6c51 100644 --- a/crates/apub/src/activities/voting/mod.rs +++ b/crates/apub/src/activities/voting/mod.rs @@ -52,7 +52,6 @@ pub(crate) async fn send_like_activity( } } -#[tracing::instrument(skip_all)] async fn vote_comment( vote_type: &VoteType, actor: ApubPerson, @@ -71,7 +70,6 @@ async fn vote_comment( Ok(()) } -#[tracing::instrument(skip_all)] async fn vote_post( vote_type: &VoteType, actor: ApubPerson, @@ -86,7 +84,6 @@ async fn vote_post( Ok(()) } -#[tracing::instrument(skip_all)] async fn undo_vote_comment( actor: ApubPerson, comment: &ApubComment, @@ -98,7 +95,6 @@ async fn undo_vote_comment( Ok(()) } -#[tracing::instrument(skip_all)] async fn undo_vote_post( actor: ApubPerson, post: &ApubPost, diff --git a/crates/apub/src/activities/voting/undo_vote.rs b/crates/apub/src/activities/voting/undo_vote.rs index f6a6039b0..1f00a5a42 100644 --- a/crates/apub/src/activities/voting/undo_vote.rs +++ b/crates/apub/src/activities/voting/undo_vote.rs @@ -53,7 +53,6 @@ impl ActivityHandler for UndoVote { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { let community = self.community(context).await?; verify_person_in_community(&self.actor, &community, context).await?; @@ -62,7 +61,6 @@ impl ActivityHandler for UndoVote { Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let actor = self.actor.dereference(context).await?; diff --git a/crates/apub/src/activities/voting/vote.rs b/crates/apub/src/activities/voting/vote.rs index bb0b32bd9..6b1aed49b 100644 --- a/crates/apub/src/activities/voting/vote.rs +++ b/crates/apub/src/activities/voting/vote.rs @@ -51,14 +51,12 @@ impl ActivityHandler for Vote { self.actor.inner() } - #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { let community = self.community(context).await?; verify_person_in_community(&self.actor, &community, context).await?; Ok(()) } - #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; let actor = self.actor.dereference(context).await?; diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index 0a11e30a0..849e27fb6 100644 --- a/crates/apub/src/activity_lists.rs +++ b/crates/apub/src/activity_lists.rs @@ -9,6 +9,7 @@ use crate::{ collection_remove::CollectionRemove, lock_page::{LockPage, UndoLockPage}, report::Report, + resolve_report::ResolveReport, update::UpdateCommunity, }, create_or_update::{note_wrapper::CreateOrUpdateNoteWrapper, page::CreateOrUpdatePage}, @@ -45,6 +46,7 @@ pub enum SharedInboxActivities { RejectFollow(RejectFollow), UndoFollow(UndoFollow), Report(Report), + ResolveReport(ResolveReport), AnnounceActivity(AnnounceActivity), /// This is a catch-all and needs to be last RawAnnouncableActivities(RawAnnouncableActivities), @@ -67,13 +69,13 @@ pub enum AnnouncableActivities { CollectionRemove(CollectionRemove), LockPost(LockPage), UndoLockPost(UndoLockPage), + Report(Report), + ResolveReport(ResolveReport), // For compatibility with Pleroma/Mastodon (send only) Page(Page), } -#[async_trait::async_trait] impl InCommunity for AnnouncableActivities { - #[tracing::instrument(skip(self, context))] async fn community(&self, context: &Data) -> LemmyResult { use AnnouncableActivities::*; match self { @@ -90,6 +92,8 @@ impl InCommunity for AnnouncableActivities { CollectionRemove(a) => a.community(context).await, LockPost(a) => a.community(context).await, UndoLockPost(a) => a.community(context).await, + Report(a) => a.community(context).await, + ResolveReport(a) => a.community(context).await, Page(_) => Err(LemmyErrorType::NotFound.into()), } } diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 45d241073..d45b811b5 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -1,13 +1,13 @@ use super::comment_sort_type_with_default; use crate::{ api::listing_type_with_default, - fetcher::resolve_actor_identifier, + fetcher::resolve_ap_identifier, objects::community::ApubCommunity, }; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; use lemmy_api_common::{ - comment::{GetComments, GetCommentsResponse}, + comment::{GetComments, GetCommentsResponse, GetCommentsSlimResponse}, context::LemmyContext, utils::{check_conflicting_like_filters, check_private_instance}, }; @@ -16,23 +16,23 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::{ - comment_view::CommentQuery, - structs::{LocalUserView, SiteView}, + comment::comment_view::CommentQuery, + structs::{CommentView, LocalUserView, SiteView}, }; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] -pub async fn list_comments( +/// A common fetcher for both the CommentView, and CommentSlimView. +async fn list_comments_common( data: Query, context: Data, local_user_view: Option, -) -> LemmyResult> { +) -> LemmyResult> { let site_view = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &site_view.local_site)?; let community_id = if let Some(name) = &data.community_name { Some( - resolve_actor_identifier::(name, &context, &local_user_view, true) + resolve_ap_identifier::(name, &context, &local_user_view, true) .await?, ) .map(|c| c.id) @@ -45,6 +45,7 @@ pub async fn list_comments( local_user_ref, &site_view.local_site, )); + let time_range_seconds = data.time_range_seconds; let max_depth = data.max_depth; let liked_only = data.liked_only; @@ -73,9 +74,10 @@ pub async fn list_comments( let post_id = data.post_id; let local_user = local_user_view.as_ref().map(|l| &l.local_user); - let comments = CommentQuery { + CommentQuery { listing_type, sort, + time_range_seconds, max_depth, liked_only, disliked_only, @@ -89,7 +91,29 @@ pub async fn list_comments( } .list(&site_view.site, &mut context.pool()) .await - .with_lemmy_type(LemmyErrorType::CouldntGetComments)?; + .with_lemmy_type(LemmyErrorType::CouldntGetComments) +} + +pub async fn list_comments( + data: Query, + context: Data, + local_user_view: Option, +) -> LemmyResult> { + let comments = list_comments_common(data, context, local_user_view).await?; Ok(Json(GetCommentsResponse { comments })) } + +pub async fn list_comments_slim( + data: Query, + context: Data, + local_user_view: Option, +) -> LemmyResult> { + let comments = list_comments_common(data, context, local_user_view) + .await? + .into_iter() + .map(CommentView::map_to_slim) + .collect(); + + Ok(Json(GetCommentsSlimResponse { comments })) +} diff --git a/crates/apub/src/api/list_person_content.rs b/crates/apub/src/api/list_person_content.rs index 774f80766..495ec6447 100644 --- a/crates/apub/src/api/list_person_content.rs +++ b/crates/apub/src/api/list_person_content.rs @@ -6,13 +6,13 @@ use lemmy_api_common::{ person::{ListPersonContent, ListPersonContentResponse}, utils::check_private_instance, }; +use lemmy_db_schema::traits::PaginationCursorBuilder; use lemmy_db_views::{ - person_content_combined_view::PersonContentCombinedQuery, - structs::{LocalUserView, SiteView}, + combined::person_content_combined_view::PersonContentCombinedQuery, + structs::{LocalUserView, PersonContentCombinedView, SiteView}, }; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn list_person_content( data: Query, context: Data, @@ -30,23 +30,22 @@ pub async fn list_person_content( ) .await?; - // parse pagination token - let page_after = if let Some(pa) = &data.page_cursor { - Some(pa.read(&mut context.pool()).await?) + let cursor_data = if let Some(cursor) = &data.page_cursor { + Some(PersonContentCombinedView::from_cursor(cursor, &mut context.pool()).await?) } else { None }; - let page_back = data.page_back; - let type_ = data.type_; let content = PersonContentCombinedQuery { creator_id: person_details_id, - type_, - page_after, - page_back, + type_: data.type_, + cursor_data, + page_back: data.page_back, } .list(&mut context.pool(), &local_user_view) .await?; - Ok(Json(ListPersonContentResponse { content })) + let next_page = content.last().map(PaginationCursorBuilder::to_cursor); + + Ok(Json(ListPersonContentResponse { content, next_page })) } diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 625661b7f..d9e93cc3a 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -1,6 +1,10 @@ use crate::{ - api::{listing_type_with_default, post_sort_type_with_default}, - fetcher::resolve_actor_identifier, + api::{ + listing_type_with_default, + post_sort_type_with_default, + post_time_range_seconds_with_default, + }, + fetcher::resolve_ap_identifier, objects::community::ApubCommunity, }; use activitypub_federation::config::Data; @@ -15,26 +19,25 @@ use lemmy_db_schema::{ source::{community::Community, post::PostRead}, }; use lemmy_db_views::{ - post_view::PostQuery, - structs::{LocalUserView, PaginationCursor, SiteView}, + post::post_view::PostQuery, + structs::{LocalUserView, PostPaginationCursor, SiteView}, }; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn list_posts( data: Query, context: Data, local_user_view: Option, ) -> LemmyResult> { - let local_site = SiteView::read_local(&mut context.pool()).await?; + let site_view = SiteView::read_local(&mut context.pool()).await?; - check_private_instance(&local_user_view, &local_site.local_site)?; + check_private_instance(&local_user_view, &site_view.local_site)?; let page = data.page; let limit = data.limit; let community_id = if let Some(name) = &data.community_name { Some( - resolve_actor_identifier::(name, &context, &local_user_view, true) + resolve_ap_identifier::(name, &context, &local_user_view, true) .await?, ) .map(|c| c.id) @@ -56,15 +59,20 @@ pub async fn list_posts( let listing_type = Some(listing_type_with_default( data.type_, local_user, - &local_site.local_site, + &site_view.local_site, community_id, )); let sort = Some(post_sort_type_with_default( data.sort, local_user, - &local_site.local_site, + &site_view.local_site, )); + let time_range_seconds = post_time_range_seconds_with_default( + data.time_range_seconds, + local_user, + &site_view.local_site, + ); // parse pagination token let page_after = if let Some(pa) = &data.page_cursor { @@ -77,6 +85,7 @@ pub async fn list_posts( local_user, listing_type, sort, + time_range_seconds, community_id, read_only, liked_only, @@ -91,7 +100,7 @@ pub async fn list_posts( no_comments_only, ..Default::default() } - .list(&local_site.site, &mut context.pool()) + .list(&site_view.site, &mut context.pool()) .await .with_lemmy_type(LemmyErrorType::CouldntGetPosts)?; @@ -107,6 +116,6 @@ pub async fn list_posts( } // 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); + let next_page = posts.last().map(PostPaginationCursor::after_post); Ok(Json(GetPostsResponse { posts, next_page })) } diff --git a/crates/apub/src/api/mod.rs b/crates/apub/src/api/mod.rs index 9359eabc4..2dc39981c 100644 --- a/crates/apub/src/api/mod.rs +++ b/crates/apub/src/api/mod.rs @@ -1,4 +1,4 @@ -use crate::{fetcher::resolve_actor_identifier, objects::person::ApubPerson}; +use crate::{fetcher::resolve_ap_identifier, objects::person::ApubPerson}; use activitypub_federation::config::Data; use lemmy_api_common::{context::LemmyContext, LemmyErrorType}; use lemmy_db_schema::{ @@ -54,6 +54,26 @@ fn post_sort_type_with_default( ) } +/// Returns a default post_time_range. +/// Order is the given, then local user default, then site default. +/// If zero is given, then the output is None. +fn post_time_range_seconds_with_default( + secs: Option, + local_user: Option<&LocalUser>, + local_site: &LocalSite, +) -> Option { + let out = secs + .or(local_user.and_then(|u| u.default_post_time_range_seconds)) + .or(local_site.default_post_time_range_seconds); + + // A zero is an override to None + if out.is_some_and(|o| o == 0) { + None + } else { + out + } +} + /// Returns a default instance-level comment sort type, if none is given by the user. /// Order is type, local user default, then site default. fn comment_sort_type_with_default( @@ -83,7 +103,7 @@ async fn resolve_person_id_from_id_or_username( Some(id) => *id, None => { if let Some(username) = username { - resolve_actor_identifier::(username, context, local_user_view, true) + resolve_ap_identifier::(username, context, local_user_view, true) .await? .id } else { diff --git a/crates/apub/src/api/read_community.rs b/crates/apub/src/api/read_community.rs index f94769158..bebec90a2 100644 --- a/crates/apub/src/api/read_community.rs +++ b/crates/apub/src/api/read_community.rs @@ -1,4 +1,4 @@ -use crate::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; +use crate::{fetcher::resolve_ap_identifier, objects::community::ApubCommunity}; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; use lemmy_api_common::{ @@ -11,11 +11,9 @@ use lemmy_db_schema::source::{ community::Community, local_site::LocalSite, }; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; +use lemmy_db_views::structs::{CommunityModeratorView, CommunityView, LocalUserView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn get_community( data: Query, context: Data, @@ -35,7 +33,7 @@ pub async fn get_community( Some(id) => id, None => { let name = data.name.clone().unwrap_or_else(|| "main".to_string()); - resolve_actor_identifier::(&name, &context, &local_user_view, true) + resolve_ap_identifier::(&name, &context, &local_user_view, true) .await? .id } @@ -59,7 +57,7 @@ pub async fn get_community( let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; - let site = read_site_for_actor(community_view.community.actor_id.clone(), &context).await?; + let site = read_site_for_actor(community_view.community.ap_id.clone(), &context).await?; let community_id = community_view.community.id; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index b79871b93..54eca0646 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -6,11 +6,9 @@ use lemmy_api_common::{ person::{GetPersonDetails, GetPersonDetailsResponse}, utils::{check_private_instance, is_admin, read_site_for_actor}, }; -use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView}; +use lemmy_db_views::structs::{CommunityModeratorView, LocalUserView, PersonView, SiteView}; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn read_person( data: Query, context: Data, @@ -42,7 +40,7 @@ pub async fn read_person( ) .await?; - let site = read_site_for_actor(person_view.person.actor_id.clone(), &context).await?; + let site = read_site_for_actor(person_view.person.ap_id.clone(), &context).await?; Ok(Json(GetPersonDetailsResponse { person_view, diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index 8d2cd384f..f259da1c9 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -11,11 +11,9 @@ use lemmy_api_common::{ utils::check_private_instance, }; use lemmy_db_schema::{source::local_site::LocalSite, utils::DbPool}; -use lemmy_db_views::structs::{CommentView, LocalUserView, PostView}; -use lemmy_db_views_actor::structs::{CommunityView, PersonView}; +use lemmy_db_views::structs::{CommentView, CommunityView, LocalUserView, PersonView, PostView}; use lemmy_utils::error::{LemmyErrorExt2, LemmyErrorType, LemmyResult}; -#[tracing::instrument(skip(context))] pub async fn resolve_object( data: Query, context: Data, diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index 0ae7053d3..6e7f80473 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -1,25 +1,18 @@ -use crate::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; +use crate::{fetcher::resolve_ap_identifier, objects::community::ApubCommunity}; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; use lemmy_api_common::{ context::LemmyContext, site::{Search, SearchResponse}, - utils::{check_conflicting_like_filters, check_private_instance, is_admin}, + utils::{check_conflicting_like_filters, check_private_instance}, }; -use lemmy_db_schema::{source::community::Community, utils::post_to_comment_sort_type, SearchType}; +use lemmy_db_schema::{source::community::Community, traits::PaginationCursorBuilder}; use lemmy_db_views::{ - comment_view::CommentQuery, - post_view::PostQuery, - structs::{LocalUserView, SiteView}, -}; -use lemmy_db_views_actor::{ - community_view::CommunityQuery, - person_view::PersonQuery, - structs::CommunitySortType, + combined::search_combined_view::SearchCombinedQuery, + structs::{LocalUserView, SearchCombinedView, SiteView}, }; use lemmy_utils::error::LemmyResult; -#[tracing::instrument(skip(context))] pub async fn search( data: Query, context: Data, @@ -28,154 +21,43 @@ pub async fn search( let local_site = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site.local_site)?; + check_conflicting_like_filters(data.liked_only, data.disliked_only)?; - let is_admin = local_user_view - .as_ref() - .map(|luv| is_admin(luv).is_ok()) - .unwrap_or_default(); - - let mut posts = Vec::new(); - let mut comments = Vec::new(); - let mut communities = Vec::new(); - let mut users = Vec::new(); - - // TODO no clean / non-nsfw searching rn - - let Query(Search { - q, - community_id, - community_name, - creator_id, - type_, - sort, - listing_type, - page, - limit, - title_only, - post_url_only, - liked_only, - disliked_only, - }) = data; - - let q = q.clone(); - let search_type = type_.unwrap_or(SearchType::All); - let community_id = if let Some(name) = &community_name { + let community_id = if let Some(name) = &data.community_name { Some( - resolve_actor_identifier::(name, &context, &local_user_view, false) + resolve_ap_identifier::(name, &context, &local_user_view, false) .await?, ) .map(|c| c.id) } else { - community_id + data.community_id }; - let local_user = local_user_view.as_ref().map(|l| &l.local_user); - check_conflicting_like_filters(liked_only, disliked_only)?; + let cursor_data = if let Some(cursor) = &data.page_cursor { + Some(SearchCombinedView::from_cursor(cursor, &mut context.pool()).await?) + } else { + None + }; - let posts_query = PostQuery { - sort, - listing_type, + let results = SearchCombinedQuery { + search_term: data.search_term.clone(), community_id, - creator_id, - local_user, - search_term: Some(q.clone()), - page, - limit, - title_only, - url_only: post_url_only, - liked_only, - disliked_only, - ..Default::default() - }; + creator_id: data.creator_id, + type_: data.type_, + sort: data.sort, + time_range_seconds: data.time_range_seconds, + listing_type: data.listing_type, + title_only: data.title_only, + post_url_only: data.post_url_only, + liked_only: data.liked_only, + disliked_only: data.disliked_only, + cursor_data, + page_back: data.page_back, + } + .list(&mut context.pool(), &local_user_view) + .await?; - let comment_query = CommentQuery { - sort: sort.map(post_to_comment_sort_type), - listing_type, - search_term: Some(q.clone()), - community_id, - creator_id, - local_user, - page, - limit, - liked_only, - disliked_only, - ..Default::default() - }; + let next_page = results.last().map(PaginationCursorBuilder::to_cursor); - let community_query = CommunityQuery { - sort: sort.map(CommunitySortType::from), - listing_type, - search_term: Some(q.clone()), - title_only, - local_user, - is_mod_or_admin: is_admin, - page, - limit, - ..Default::default() - }; - - let person_query = PersonQuery { - sort, - search_term: Some(q.clone()), - listing_type, - page, - limit, - }; - - match search_type { - SearchType::Posts => { - posts = posts_query - .list(&local_site.site, &mut context.pool()) - .await?; - } - SearchType::Comments => { - comments = comment_query - .list(&local_site.site, &mut context.pool()) - .await?; - } - SearchType::Communities => { - communities = community_query - .list(&local_site.site, &mut context.pool()) - .await?; - } - SearchType::Users => { - users = person_query.list(&mut context.pool()).await?; - } - SearchType::All => { - // If the community or creator is included, dont search communities or users - let community_or_creator_included = - community_id.is_some() || community_name.is_some() || creator_id.is_some(); - - posts = posts_query - .list(&local_site.site, &mut context.pool()) - .await?; - - comments = comment_query - .list(&local_site.site, &mut context.pool()) - .await?; - - communities = if community_or_creator_included { - vec![] - } else { - community_query - .list(&local_site.site, &mut context.pool()) - .await? - }; - - users = if community_or_creator_included { - vec![] - } else { - person_query.list(&mut context.pool()).await? - }; - } - }; - - // Return the jwt - Ok(Json(SearchResponse { - type_: search_type, - comments, - posts, - communities, - users, - })) + Ok(Json(SearchResponse { results, next_page })) } diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index d98df25ad..1694322ca 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -18,7 +18,6 @@ use lemmy_db_schema::{ instance::Instance, instance_block::{InstanceBlock, InstanceBlockForm}, local_user::{LocalUser, LocalUserUpdateForm}, - local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeUpdateForm}, person::{Person, PersonUpdateForm}, person_block::{PersonBlock, PersonBlockForm}, post::{PostSaved, PostSavedForm}, @@ -55,7 +54,6 @@ pub struct UserSettingsBackup { // TODO: might be worth making a separate struct for settings backup, to avoid breakage in case // fields are renamed, and to avoid storing unnecessary fields like person_id or email pub settings: Option, - pub vote_display_mode_settings: Option, #[serde(default)] pub followed_communities: Vec>, #[serde(default)] @@ -70,7 +68,6 @@ pub struct UserSettingsBackup { pub blocked_instances: Vec, } -#[tracing::instrument(skip(context))] pub async fn export_settings( local_user_view: LocalUserView, context: Data, @@ -86,7 +83,6 @@ pub async fn export_settings( matrix_id: local_user_view.person.matrix_user_id, bot_account: local_user_view.person.bot_account.into(), settings: Some(local_user_view.local_user), - vote_display_mode_settings: Some(local_user_view.local_user_vote_display_mode), followed_communities: vec_into(lists.followed_communities), blocked_communities: vec_into(lists.blocked_communities), blocked_instances: lists.blocked_instances, @@ -96,7 +92,6 @@ pub async fn export_settings( })) } -#[tracing::instrument(skip(context))] pub async fn import_settings( data: Json, local_user_view: LocalUserView, @@ -105,7 +100,7 @@ pub async fn import_settings( let person_form = PersonUpdateForm { display_name: data.display_name.clone().map(Some), bio: data.bio.clone().map(Some), - matrix_user_id: data.bio.clone().map(Some), + matrix_user_id: data.matrix_id.clone().map(Some), bot_account: data.bot_account, ..Default::default() }; @@ -132,6 +127,10 @@ pub async fn import_settings( blur_nsfw: data.settings.as_ref().map(|s| s.blur_nsfw), infinite_scroll_enabled: data.settings.as_ref().map(|s| s.infinite_scroll_enabled), post_listing_mode: data.settings.as_ref().map(|s| s.post_listing_mode), + show_score: data.settings.as_ref().map(|s| s.show_score), + show_upvotes: data.settings.as_ref().map(|s| s.show_upvotes), + show_downvotes: data.settings.as_ref().map(|s| s.show_downvotes), + show_upvote_percentage: data.settings.as_ref().map(|s| s.show_upvote_percentage), ..Default::default() }; LocalUser::update( @@ -141,27 +140,6 @@ pub async fn import_settings( ) .await?; - // Update the vote display mode settings - let vote_display_mode_form = LocalUserVoteDisplayModeUpdateForm { - score: data.vote_display_mode_settings.as_ref().map(|s| s.score), - upvotes: data.vote_display_mode_settings.as_ref().map(|s| s.upvotes), - downvotes: data - .vote_display_mode_settings - .as_ref() - .map(|s| s.downvotes), - upvote_percentage: data - .vote_display_mode_settings - .as_ref() - .map(|s| s.upvote_percentage), - }; - - LocalUserVoteDisplayMode::update( - &mut context.pool(), - local_user_view.local_user.id, - &vote_display_mode_form, - ) - .await?; - let url_count = data.followed_communities.len() + data.blocked_communities.len() + data.blocked_users.len() @@ -323,8 +301,7 @@ pub(crate) mod tests { }, traits::{Crud, Followable}, }; - use lemmy_db_views::structs::LocalUserView; - use lemmy_db_views_actor::structs::CommunityFollowerView; + use lemmy_db_views::structs::{CommunityFollowerView, LocalUserView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serial_test::serial; use std::time::Duration; @@ -371,7 +348,7 @@ pub(crate) mod tests { let follows = CommunityFollowerView::for_person(pool, import_user.person.id).await?; assert_eq!(follows.len(), 1); - assert_eq!(follows[0].community.actor_id, community.actor_id); + assert_eq!(follows[0].community.ap_id, community.ap_id); Person::delete(pool, export_user.person.id).await?; Person::delete(pool, import_user.person.id).await?; diff --git a/crates/apub/src/collections/community_featured.rs b/crates/apub/src/collections/community_featured.rs index e092693e6..6e496d2fe 100644 --- a/crates/apub/src/collections/community_featured.rs +++ b/crates/apub/src/collections/community_featured.rs @@ -41,7 +41,7 @@ impl Collection for ApubCommunityFeatured { .await?; Ok(GroupFeatured { r#type: OrderedCollectionType::OrderedCollection, - id: generate_featured_url(&owner.actor_id)?.into(), + id: generate_featured_url(&owner.ap_id)?.into(), total_items: ordered_items.len() as i32, ordered_items, }) diff --git a/crates/apub/src/collections/community_follower.rs b/crates/apub/src/collections/community_follower.rs index a4f5debbc..5862a359e 100644 --- a/crates/apub/src/collections/community_follower.rs +++ b/crates/apub/src/collections/community_follower.rs @@ -9,8 +9,8 @@ use activitypub_federation::{ traits::Collection, }; use lemmy_api_common::{context::LemmyContext, utils::generate_followers_url}; -use lemmy_db_schema::aggregates::structs::CommunityAggregates; -use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_db_schema::source::community::Community; +use lemmy_db_views::structs::CommunityFollowerView; use lemmy_utils::error::LemmyError; use url::Url; @@ -33,7 +33,7 @@ impl Collection for ApubCommunityFollower { CommunityFollowerView::count_community_followers(&mut context.pool(), community_id).await?; Ok(GroupFollowers { - id: generate_followers_url(&community.actor_id)?.into(), + id: generate_followers_url(&community.ap_id)?.into(), r#type: CollectionType::Collection, total_items: community_followers as i32, items: vec![], @@ -54,12 +54,8 @@ impl Collection for ApubCommunityFollower { community: &Self::Owner, context: &Data, ) -> Result { - CommunityAggregates::update_federated_followers( - &mut context.pool(), - community.id, - json.total_items, - ) - .await?; + Community::update_federated_followers(&mut context.pool(), community.id, json.total_items) + .await?; Ok(ApubCommunityFollower(())) } diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs index c7b925f97..f2c667bd9 100644 --- a/crates/apub/src/collections/community_moderators.rs +++ b/crates/apub/src/collections/community_moderators.rs @@ -14,7 +14,7 @@ use lemmy_db_schema::{ source::community::{CommunityModerator, CommunityModeratorForm}, traits::Joinable, }; -use lemmy_db_views_actor::structs::CommunityModeratorView; +use lemmy_db_views::structs::CommunityModeratorView; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; @@ -28,21 +28,19 @@ impl Collection for ApubCommunityModerators { type Kind = GroupModerators; type Error = LemmyError; - #[tracing::instrument(skip_all)] async fn read_local(owner: &Self::Owner, data: &Data) -> LemmyResult { let moderators = CommunityModeratorView::for_community(&mut data.pool(), owner.id).await?; let ordered_items = moderators .into_iter() - .map(|m| ObjectId::::from(m.moderator.actor_id)) + .map(|m| ObjectId::::from(m.moderator.ap_id)) .collect(); Ok(GroupModerators { r#type: OrderedCollectionType::OrderedCollection, - id: generate_moderators_url(&owner.actor_id)?.into(), + id: generate_moderators_url(&owner.ap_id)?.into(), ordered_items, }) } - #[tracing::instrument(skip_all)] async fn verify( group_moderators: &GroupModerators, expected_domain: &Url, @@ -52,7 +50,6 @@ impl Collection for ApubCommunityModerators { Ok(()) } - #[tracing::instrument(skip_all)] async fn from_json( apub: Self::Kind, owner: &Self::Owner, @@ -63,7 +60,7 @@ impl Collection for ApubCommunityModerators { CommunityModeratorView::for_community(&mut data.pool(), community_id).await?; // Remove old mods from database which arent in the moderators collection anymore for mod_user in ¤t_moderators { - let mod_id = ObjectId::from(mod_user.moderator.actor_id.clone()); + let mod_id = ObjectId::from(mod_user.moderator.ap_id.clone()); if !apub.ordered_items.contains(&mod_id) { let community_moderator_form = CommunityModeratorForm { community_id: mod_user.community.id, @@ -80,8 +77,8 @@ impl Collection for ApubCommunityModerators { if let Some(mod_user) = mod_user { if !current_moderators .iter() - .map(|c| c.moderator.actor_id.clone()) - .any(|x| x == mod_user.actor_id) + .map(|c| c.moderator.ap_id.clone()) + .any(|x| x == mod_user.ap_id) { let community_moderator_form = CommunityModeratorForm { community_id: owner.id, @@ -139,7 +136,7 @@ mod tests { CommunityModerator::join(&mut context.pool(), &community_moderator_form).await?; - assert_eq!(site.actor_id.to_string(), "https://enterprise.lemmy.ml/"); + assert_eq!(site.ap_id.to_string(), "https://enterprise.lemmy.ml/"); let json: GroupModerators = file_to_json_object("assets/lemmy/collections/group_moderators.json")?; diff --git a/crates/apub/src/collections/community_outbox.rs b/crates/apub/src/collections/community_outbox.rs index 01199bc2b..3cab387ff 100644 --- a/crates/apub/src/collections/community_outbox.rs +++ b/crates/apub/src/collections/community_outbox.rs @@ -19,7 +19,7 @@ use activitypub_federation::{ use futures::future::join_all; use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url}; use lemmy_db_schema::{source::site::Site, utils::FETCH_LIMIT_MAX, PostSortType}; -use lemmy_db_views::post_view::PostQuery; +use lemmy_db_views::post::post_view::PostQuery; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; @@ -33,7 +33,6 @@ impl Collection for ApubCommunityOutbox { type Kind = GroupOutbox; type Error = LemmyError; - #[tracing::instrument(skip_all)] async fn read_local(owner: &Self::Owner, data: &Data) -> LemmyResult { let site = Site::read_local(&mut data.pool()).await?; @@ -48,28 +47,31 @@ impl Collection for ApubCommunityOutbox { let mut ordered_items = vec![]; for post_view in post_views { - let create = CreateOrUpdatePage::new( + // ignore errors, in particular if post creator was deleted + if let Ok(create) = CreateOrUpdatePage::new( post_view.post.into(), &post_view.creator.into(), owner, CreateOrUpdateType::Create, data, ) - .await?; - let announcable = AnnouncableActivities::CreateOrUpdatePost(create); - let announce = AnnounceActivity::new(announcable.try_into()?, owner, data)?; - ordered_items.push(announce); + .await + { + let announcable = AnnouncableActivities::CreateOrUpdatePost(create); + if let Ok(announce) = AnnounceActivity::new(announcable.try_into()?, owner, data) { + ordered_items.push(announce); + } + } } Ok(GroupOutbox { r#type: OrderedCollectionType::OrderedCollection, - id: generate_outbox_url(&owner.actor_id)?.into(), + id: generate_outbox_url(&owner.ap_id)?.into(), total_items: ordered_items.len() as i32, ordered_items, }) } - #[tracing::instrument(skip_all)] async fn verify( group_outbox: &GroupOutbox, expected_domain: &Url, @@ -79,7 +81,6 @@ impl Collection for ApubCommunityOutbox { Ok(()) } - #[tracing::instrument(skip_all)] async fn from_json( apub: Self::Kind, _owner: &Self::Owner, diff --git a/crates/apub/src/fetcher/markdown_links.rs b/crates/apub/src/fetcher/markdown_links.rs index a5e51caa7..78e81a8cc 100644 --- a/crates/apub/src/fetcher/markdown_links.rs +++ b/crates/apub/src/fetcher/markdown_links.rs @@ -1,10 +1,7 @@ use super::{search::SearchableObjects, user_or_community::UserOrCommunity}; use crate::fetcher::post_or_comment::PostOrComment; use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; -use lemmy_api_common::{ - context::LemmyContext, - utils::{generate_local_apub_endpoint, EndpointType}, -}; +use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{newtypes::InstanceId, source::instance::Instance}; use lemmy_utils::{ error::LemmyResult, @@ -61,12 +58,8 @@ pub(crate) async fn to_local_url(url: &str, context: &Data) -> Opt let dereferenced = object_id.dereference(context).await.ok()?; match dereferenced { SearchableObjects::PostOrComment(pc) => match *pc { - PostOrComment::Post(post) => { - generate_local_apub_endpoint(EndpointType::Post, &post.id.to_string(), local_domain) - } - PostOrComment::Comment(comment) => { - generate_local_apub_endpoint(EndpointType::Comment, &comment.id.to_string(), local_domain) - } + PostOrComment::Post(post) => post.local_url(context.settings()), + PostOrComment::Comment(comment) => comment.local_url(context.settings()), } .ok() .map(Into::into), @@ -150,7 +143,7 @@ mod tests { ), ( "rewrite community link", - format!("[link]({})", community.actor_id), + format!("[link]({})", community.ap_id), "[link](https://lemmy-alpha/c/my_community@example.com)", ), ( diff --git a/crates/apub/src/fetcher/mod.rs b/crates/apub/src/fetcher/mod.rs index b2bc35672..a6b2f24ca 100644 --- a/crates/apub/src/fetcher/mod.rs +++ b/crates/apub/src/fetcher/mod.rs @@ -20,8 +20,7 @@ pub mod user_or_community; /// /// In case the requesting user is logged in and the object was not found locally, it is attempted /// to fetch via webfinger from the original instance. -#[tracing::instrument(skip_all)] -pub async fn resolve_actor_identifier( +pub async fn resolve_ap_identifier( identifier: &str, context: &Data, local_user_view: &Option, diff --git a/crates/apub/src/fetcher/post_or_comment.rs b/crates/apub/src/fetcher/post_or_comment.rs index be48e8ebd..0f6f31d8f 100644 --- a/crates/apub/src/fetcher/post_or_comment.rs +++ b/crates/apub/src/fetcher/post_or_comment.rs @@ -39,7 +39,6 @@ impl Object for PostOrComment { None } - #[tracing::instrument(skip_all)] async fn read_from_id(object_id: Url, data: &Data) -> LemmyResult> { let post = ApubPost::read_from_id(object_id.clone(), data).await?; Ok(match post { @@ -50,7 +49,6 @@ impl Object for PostOrComment { }) } - #[tracing::instrument(skip_all)] async fn delete(self, data: &Data) -> LemmyResult<()> { match self { PostOrComment::Post(p) => p.delete(data).await, @@ -65,7 +63,6 @@ impl Object for PostOrComment { }) } - #[tracing::instrument(skip_all)] async fn verify( apub: &Self::Kind, expected_domain: &Url, @@ -77,7 +74,6 @@ impl Object for PostOrComment { } } - #[tracing::instrument(skip_all)] async fn from_json(apub: PageOrNote, context: &Data) -> LemmyResult { Ok(match apub { PageOrNote::Page(p) => PostOrComment::Post(ApubPost::from_json(*p, context).await?), @@ -86,7 +82,6 @@ impl Object for PostOrComment { } } -#[async_trait::async_trait] impl InCommunity for PostOrComment { async fn community(&self, context: &Data) -> LemmyResult { let cid = match self { diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index e8c029106..769b4c9cd 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -14,7 +14,6 @@ use url::Url; /// Converts search query to object id. The query can either be an URL, which will be treated as /// ObjectId directly, or a webfinger identifier (@user@example.com or !community@example.com) /// which gets resolved to an URL. -#[tracing::instrument(skip_all)] pub(crate) async fn search_query_to_object_id( mut query: String, context: &Data, @@ -39,7 +38,6 @@ pub(crate) async fn search_query_to_object_id( /// Converts a search query to an object id. The query MUST bbe a URL which will bbe treated /// as the ObjectId directly. If the query is a webfinger identifier (@user@example.com or /// !community@example.com) this method will return an error. -#[tracing::instrument(skip_all)] pub(crate) async fn search_query_to_object_id_local( query: &str, context: &Data, @@ -80,7 +78,6 @@ impl Object for SearchableObjects { // a single query. // we could skip this and always return an error, but then it would always fetch objects // over http, and not be able to mark objects as deleted that were deleted by remote server. - #[tracing::instrument(skip_all)] async fn read_from_id( object_id: Url, context: &Data, @@ -96,7 +93,6 @@ impl Object for SearchableObjects { Ok(None) } - #[tracing::instrument(skip_all)] async fn delete(self, data: &Data) -> LemmyResult<()> { match self { SearchableObjects::PostOrComment(pc) => pc.delete(data).await, @@ -112,7 +108,6 @@ impl Object for SearchableObjects { }) } - #[tracing::instrument(skip_all)] async fn verify( apub: &Self::Kind, expected_domain: &Url, @@ -125,7 +120,6 @@ impl Object for SearchableObjects { } } - #[tracing::instrument(skip_all)] async fn from_json(apub: Self::Kind, context: &Data) -> LemmyResult { use SearchableKinds::*; use SearchableObjects as SO; diff --git a/crates/apub/src/fetcher/site_or_community_or_user.rs b/crates/apub/src/fetcher/site_or_community_or_user.rs index 79d7978ae..5d639fd4a 100644 --- a/crates/apub/src/fetcher/site_or_community_or_user.rs +++ b/crates/apub/src/fetcher/site_or_community_or_user.rs @@ -41,7 +41,6 @@ impl Object for SiteOrCommunityOrUser { }) } - #[tracing::instrument(skip_all)] async fn read_from_id(object_id: Url, data: &Data) -> LemmyResult> { let site = ApubSite::read_from_id(object_id.clone(), data).await?; Ok(match site { @@ -52,7 +51,6 @@ impl Object for SiteOrCommunityOrUser { }) } - #[tracing::instrument(skip_all)] async fn delete(self, data: &Data) -> LemmyResult<()> { match self { SiteOrCommunityOrUser::Site(p) => p.delete(data).await, @@ -69,7 +67,6 @@ impl Object for SiteOrCommunityOrUser { }) } - #[tracing::instrument(skip_all)] async fn verify( apub: &Self::Kind, expected_domain: &Url, @@ -83,7 +80,6 @@ impl Object for SiteOrCommunityOrUser { } } - #[tracing::instrument(skip_all)] async fn from_json(apub: Self::Kind, data: &Data) -> LemmyResult { Ok(match apub { SiteOrPersonOrGroup::Instance(a) => { diff --git a/crates/apub/src/fetcher/user_or_community.rs b/crates/apub/src/fetcher/user_or_community.rs index 129af8803..a2d8e0991 100644 --- a/crates/apub/src/fetcher/user_or_community.rs +++ b/crates/apub/src/fetcher/user_or_community.rs @@ -46,7 +46,6 @@ impl Object for UserOrCommunity { }) } - #[tracing::instrument(skip_all)] async fn read_from_id(object_id: Url, data: &Data) -> LemmyResult> { let person = ApubPerson::read_from_id(object_id.clone(), data).await?; Ok(match person { @@ -57,7 +56,6 @@ impl Object for UserOrCommunity { }) } - #[tracing::instrument(skip_all)] async fn delete(self, data: &Data) -> LemmyResult<()> { match self { UserOrCommunity::User(p) => p.delete(data).await, @@ -72,7 +70,6 @@ impl Object for UserOrCommunity { }) } - #[tracing::instrument(skip_all)] async fn verify( apub: &Self::Kind, expected_domain: &Url, @@ -84,7 +81,6 @@ impl Object for UserOrCommunity { } } - #[tracing::instrument(skip_all)] async fn from_json(apub: Self::Kind, data: &Data) -> LemmyResult { Ok(match apub { PersonOrGroup::Person(p) => UserOrCommunity::User(ApubPerson::from_json(p, data).await?), diff --git a/crates/apub/src/http/comment.rs b/crates/apub/src/http/comment.rs index 41160234f..710e1862b 100644 --- a/crates/apub/src/http/comment.rs +++ b/crates/apub/src/http/comment.rs @@ -20,7 +20,6 @@ pub(crate) struct CommentQuery { } /// Return the ActivityPub json representation of a local comment over HTTP. -#[tracing::instrument(skip_all)] pub(crate) async fn get_apub_comment( info: Path, context: Data, diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index dbcc51258..7121b453e 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -23,7 +23,7 @@ use actix_web::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::ApubActor, CommunityVisibility}; -use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_db_views::structs::CommunityFollowerView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serde::Deserialize; @@ -38,7 +38,6 @@ pub struct CommunityIsFollowerQuery { } /// Return the ActivityPub json representation of a local community over HTTP. -#[tracing::instrument(skip_all)] pub(crate) async fn get_apub_community_http( info: Path, context: Data, @@ -50,7 +49,7 @@ pub(crate) async fn get_apub_community_http( .into(); if community.deleted || community.removed { - return create_apub_tombstone_response(community.actor_id.clone()); + return create_apub_tombstone_response(community.ap_id.clone()); } check_community_fetchable(&community)?; @@ -126,7 +125,6 @@ pub(crate) async fn get_apub_community_outbox( create_apub_response(&outbox) } -#[tracing::instrument(skip_all)] pub(crate) async fn get_apub_community_moderators( info: Path, context: Data, @@ -170,6 +168,8 @@ pub(crate) mod tests { instance::Instance, local_site::{LocalSite, LocalSiteInsertForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm}, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, site::{Site, SiteInsertForm}, }, traits::Crud, @@ -182,7 +182,7 @@ pub(crate) mod tests { deleted: bool, visibility: CommunityVisibility, context: &Data, - ) -> LemmyResult<(Instance, Community)> { + ) -> LemmyResult<(Instance, Community, Path)> { let instance = Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?; create_local_site(context, instance.id).await?; @@ -198,7 +198,11 @@ pub(crate) mod tests { ) }; let community = Community::create(&mut context.pool(), &community_form).await?; - Ok((instance, community)) + let path: Path = CommunityPath { + community_name: community.name.clone(), + } + .into(); + Ok((instance, community, path)) } /// Necessary for the community outbox fetching @@ -228,7 +232,7 @@ pub(crate) mod tests { #[serial] async fn test_get_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; - let (instance, community) = init(false, CommunityVisibility::Public, &context).await?; + let (instance, community, path) = init(false, CommunityVisibility::Public, &context).await?; let request = TestRequest::default().to_http_request(); // fetch invalid community @@ -239,9 +243,6 @@ pub(crate) mod tests { assert!(res.is_err()); // fetch valid community - let path = CommunityPath { - community_name: community.name.clone(), - }; 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?; @@ -268,8 +269,7 @@ pub(crate) mod tests { let res = get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await?; assert_eq!(200, res.status()); - let res = - get_apub_community_outbox(path.into(), context.reset_request_count(), request).await?; + let res = get_apub_community_outbox(path, context.reset_request_count(), request).await?; assert_eq!(200, res.status()); Instance::delete(&mut context.pool(), instance.id).await?; @@ -280,14 +280,10 @@ pub(crate) mod tests { #[serial] async fn test_get_deleted_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; - let (instance, community) = init(true, CommunityVisibility::LocalOnly, &context).await?; + let (instance, _, path) = init(true, CommunityVisibility::LocalOnly, &context).await?; let request = TestRequest::default().to_http_request(); // should return tombstone - let path: Path = CommunityPath { - community_name: community.name.clone(), - } - .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; @@ -324,13 +320,9 @@ pub(crate) mod tests { #[serial] async fn test_get_local_only_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; - let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?; + let (instance, _, path) = init(false, CommunityVisibility::LocalOnly, &context).await?; let request = TestRequest::default().to_http_request(); - let path: Path = CommunityPath { - community_name: community.name.clone(), - } - .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( @@ -358,4 +350,26 @@ pub(crate) mod tests { Instance::delete(&mut context.pool(), instance.id).await?; Ok(()) } + + #[tokio::test] + #[serial] + async fn test_outbox_deleted_user() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let (instance, community, path) = init(false, CommunityVisibility::Public, &context).await?; + let request = TestRequest::default().to_http_request(); + + // post from deleted user shouldnt break outbox + let mut form = PersonInsertForm::new("jerry".to_string(), String::new(), instance.id); + form.deleted = Some(true); + let person = Person::create(&mut context.pool(), &form).await?; + + let form = PostInsertForm::new("title".to_string(), person.id, community.id); + Post::create(&mut context.pool(), &form).await?; + + let res = get_apub_community_outbox(path, context.reset_request_count(), request).await?; + assert_eq!(200, res.status()); + + Instance::delete(&mut context.pool(), instance.id).await?; + Ok(()) + } } diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index 52e036376..13dc02696 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -18,7 +18,7 @@ use lemmy_db_schema::{ source::{activity::SentActivity, community::Community}, CommunityVisibility, }; -use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_db_views::structs::CommunityFollowerView; use lemmy_utils::error::{FederationError, LemmyErrorExt, LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; use std::{ops::Deref, time::Duration}; @@ -95,7 +95,6 @@ pub struct ActivityQuery { } /// Return the ActivityPub json representation of a local activity over HTTP. -#[tracing::instrument(skip_all)] pub(crate) async fn get_activity( info: web::Path, context: web::Data, diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index f8afceb94..ca1b3af65 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -16,7 +16,6 @@ pub struct PersonQuery { } /// Return the ActivityPub json representation of a local person over HTTP. -#[tracing::instrument(skip_all)] pub(crate) async fn get_apub_person_http( info: web::Path, context: Data, @@ -33,11 +32,10 @@ pub(crate) async fn get_apub_person_http( create_apub_response(&apub) } else { - create_apub_tombstone_response(person.actor_id.clone()) + create_apub_tombstone_response(person.ap_id.clone()) } } -#[tracing::instrument(skip_all)] pub(crate) async fn get_apub_person_outbox( info: web::Path, context: Data, @@ -45,7 +43,7 @@ pub(crate) async fn get_apub_person_outbox( let person = Person::read_from_name(&mut context.pool(), &info.user_name, false) .await? .ok_or(LemmyErrorType::NotFound)?; - let outbox_id = generate_outbox_url(&person.actor_id)?.into(); + let outbox_id = generate_outbox_url(&person.ap_id)?.into(); let outbox = EmptyOutbox::new(outbox_id)?; create_apub_response(&outbox) } diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs index 6afb9fc3e..fea05f5df 100644 --- a/crates/apub/src/http/post.rs +++ b/crates/apub/src/http/post.rs @@ -20,7 +20,6 @@ pub(crate) struct PostQuery { } /// Return the ActivityPub json representation of a local post over HTTP. -#[tracing::instrument(skip_all)] pub(crate) async fn get_apub_post( info: web::Path, context: Data, diff --git a/crates/apub/src/http/site.rs b/crates/apub/src/http/site.rs index 95175a006..ff2169d7c 100644 --- a/crates/apub/src/http/site.rs +++ b/crates/apub/src/http/site.rs @@ -17,7 +17,6 @@ pub(crate) async fn get_apub_site_http(context: Data) -> LemmyResu create_apub_response(&apub) } -#[tracing::instrument(skip_all)] pub(crate) async fn get_apub_site_outbox(context: Data) -> LemmyResult { let outbox_id = format!( "{}/site_outbox", diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 028d673c2..aeb5a8d05 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -17,6 +17,7 @@ use lemmy_utils::{ use moka::future::Cache; use serde_json::Value; use std::sync::{Arc, LazyLock}; +use tracing::debug; use url::Url; pub mod activities; @@ -89,7 +90,6 @@ impl UrlVerifier for VerifyUrlData { /// - the correct scheme (either http or https) /// - URL being in the allowlist (if it is active) /// - URL not being in the blocklist (if it is active) -#[tracing::instrument(skip(local_site_data))] fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> LemmyResult<()> { let domain = apub_id .domain() @@ -213,8 +213,8 @@ pub(crate) async fn check_apub_id_valid_with_strictness( /// /// This ensures that the same activity doesn't get received and processed more than once, which /// would be a waste of resources. -#[tracing::instrument(skip(data))] async fn insert_received_activity(ap_id: &Url, data: &Data) -> LemmyResult<()> { + debug!("Received activity {}", ap_id.to_string()); ReceivedActivity::create(&mut data.pool(), &ap_id.clone().into()).await?; Ok(()) } diff --git a/crates/apub/src/mentions.rs b/crates/apub/src/mentions.rs index 0f04818a3..ecdf8c1a0 100644 --- a/crates/apub/src/mentions.rs +++ b/crates/apub/src/mentions.rs @@ -42,7 +42,6 @@ pub struct MentionsAndAddresses { /// This takes a comment, and builds a list of to_addresses, inboxes, /// and mention tags, so they know where to be sent to. /// Addresses are the persons / addresses that go in the cc field. -#[tracing::instrument(skip(comment, context))] pub async fn collect_non_local_mentions( comment: &ApubComment, context: &Data, @@ -52,7 +51,7 @@ pub async fn collect_non_local_mentions( // Add the mention tag let parent_creator_tag = Mention { - href: parent_creator.actor_id.clone().into(), + href: parent_creator.ap_id.clone().into(), name: Some(format!( "@{}@{}", &parent_creator.name, @@ -75,7 +74,7 @@ pub async fn collect_non_local_mentions( let identifier = format!("{}@{}", mention.name, mention.domain); let person = webfinger_resolve_actor::(&identifier, context).await; if let Ok(person) = person { - addressed_ccs.push(person.actor_id.to_string().parse()?); + addressed_ccs.push(person.ap_id.to_string().parse()?); let mention_tag = Mention { href: person.id(), @@ -95,7 +94,6 @@ pub async fn collect_non_local_mentions( /// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a /// top-level comment, the creator of the post, otherwise the creator of the parent comment. -#[tracing::instrument(skip(pool, comment))] async fn get_comment_parent_creator( pool: &mut DbPool<'_>, comment: &Comment, diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index fd168e370..609350fa5 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -3,7 +3,7 @@ use crate::{ check_apub_id_valid_with_strictness, fetcher::markdown_links::markdown_rewrite_remote_links, mentions::collect_non_local_mentions, - objects::{append_attachments_to_comment, read_from_string_or_source, verify_is_remote_object}, + objects::{append_attachments_to_comment, read_from_string_or_source}, protocol::{ objects::{note::Note, LanguageTag}, InCommunity, @@ -13,19 +13,21 @@ use crate::{ use activitypub_federation::{ config::Data, kinds::object::NoteType, - protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match}, + protocol::{ + values::MediaTypeMarkdownOrHtml, + verification::{verify_domains_match, verify_is_remote_object}, + }, traits::Object, }; use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, - utils::{get_url_blocklist, is_mod_or_admin, local_site_opt_to_slur_regex, process_markdown}, + utils::{get_url_blocklist, is_mod_or_admin, process_markdown, slur_regex}, }; use lemmy_db_schema::{ source::{ comment::{Comment, CommentInsertForm, CommentUpdateForm}, community::Community, - local_site::LocalSite, person::Person, post::Post, }, @@ -64,7 +66,6 @@ impl Object for ApubComment { None } - #[tracing::instrument(skip_all)] async fn read_from_id( object_id: Url, context: &Data, @@ -76,7 +77,6 @@ impl Object for ApubComment { ) } - #[tracing::instrument(skip_all)] async fn delete(self, context: &Data) -> LemmyResult<()> { if !self.deleted { let form = CommentUpdateForm { @@ -88,7 +88,6 @@ impl Object for ApubComment { Ok(()) } - #[tracing::instrument(skip_all)] async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; let creator = Person::read(&mut context.pool(), creator_id).await?; @@ -110,7 +109,7 @@ impl Object for ApubComment { let note = Note { r#type: NoteType::Note, id: self.ap_id.clone().into(), - attributed_to: creator.actor_id.into(), + attributed_to: creator.ap_id.into(), to: generate_to(&community)?, cc: maa.ccs, content: markdown_to_html(&self.content), @@ -130,7 +129,6 @@ impl Object for ApubComment { /// Recursively fetches all parent comments. This can lead to a stack overflow so we need to /// Box::pin all large futures on the heap. - #[tracing::instrument(skip_all)] async fn verify( note: &Note, expected_domain: &Url, @@ -170,18 +168,16 @@ impl Object for ApubComment { /// Converts a `Note` to `Comment`. /// /// If the parent community, post and comment(s) are not known locally, these are also fetched. - #[tracing::instrument(skip_all)] async fn from_json(note: Note, context: &Data) -> LemmyResult { let creator = note.attributed_to.dereference(context).await?; let (post, parent_comment) = note.get_parents(context).await?; let content = read_from_string_or_source(¬e.content, ¬e.media_type, ¬e.source); - let local_site = LocalSite::read(&mut context.pool()).await.ok(); - let slur_regex = &local_site_opt_to_slur_regex(&local_site); + let slur_regex = slur_regex(context).await?; let url_blocklist = get_url_blocklist(context).await?; let content = append_attachments_to_comment(content, ¬e.attachment, context).await?; - let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?; + let content = process_markdown(&content, &slur_regex, &url_blocklist, context).await?; let content = markdown_rewrite_remote_links(content, context).await; let language_id = Some( LanguageTag::to_language_id_single(note.language.unwrap_or_default(), &mut context.pool()) @@ -228,7 +224,7 @@ pub(crate) mod tests { }; use assert_json_diff::assert_json_include; use html2md::parse_html; - use lemmy_db_schema::source::site::Site; + use lemmy_db_schema::source::{local_site::LocalSite, site::Site}; use pretty_assertions::assert_eq; use serial_test::serial; diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 689641910..0376ccfaa 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -1,8 +1,6 @@ use crate::{ activities::GetActorType, - check_apub_id_valid, fetcher::markdown_links::markdown_rewrite_remote_links_opt, - local_site_data_cached, objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt}, protocol::{ objects::{group::Group, LanguageTag}, @@ -20,13 +18,14 @@ use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, utils::{ + check_nsfw_allowed, generate_featured_url, generate_moderators_url, generate_outbox_url, get_url_blocklist, - local_site_opt_to_slur_regex, process_markdown_opt, proxy_image_link_opt_apub, + slur_regex, }, }; use lemmy_db_schema::{ @@ -40,7 +39,6 @@ use lemmy_db_schema::{ traits::{ApubActor, Crud}, CommunityVisibility, }; -use lemmy_db_views_actor::structs::CommunityFollowerView; use lemmy_utils::{ error::{LemmyError, LemmyResult}, spawn_try_task, @@ -75,7 +73,6 @@ impl Object for ApubCommunity { Some(self.last_refreshed_at) } - #[tracing::instrument(skip_all)] async fn read_from_id( object_id: Url, context: &Data, @@ -87,7 +84,6 @@ impl Object for ApubCommunity { ) } - #[tracing::instrument(skip_all)] async fn delete(self, context: &Data) -> LemmyResult<()> { let form = CommunityUpdateForm { deleted: Some(true), @@ -97,7 +93,6 @@ impl Object for ApubCommunity { Ok(()) } - #[tracing::instrument(skip_all)] async fn into_json(self, data: &Data) -> LemmyResult { let community_id = self.id; let langs = CommunityLanguage::read(&mut data.pool(), community_id).await?; @@ -115,9 +110,9 @@ impl Object for ApubCommunity { icon: self.icon.clone().map(ImageObject::new), image: self.banner.clone().map(ImageObject::new), sensitive: Some(self.nsfw), - featured: Some(generate_featured_url(&self.actor_id)?.into()), + featured: Some(generate_featured_url(&self.ap_id)?.into()), inbox: self.inbox_url.clone().into(), - outbox: generate_outbox_url(&self.actor_id)?.into(), + outbox: generate_outbox_url(&self.ap_id)?.into(), followers: self.followers_url.clone().map(Into::into), endpoints: None, public_key: self.public_key(), @@ -125,13 +120,12 @@ impl Object for ApubCommunity { published: Some(self.published), updated: self.updated, posting_restricted_to_mods: Some(self.posting_restricted_to_mods), - attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()), + attributed_to: Some(generate_moderators_url(&self.ap_id)?.into()), manually_approves_followers: Some(self.visibility == CommunityVisibility::Private), }; Ok(group) } - #[tracing::instrument(skip_all)] async fn verify( group: &Group, expected_domain: &Url, @@ -141,15 +135,14 @@ impl Object for ApubCommunity { } /// Converts a `Group` to `Community`, inserts it into the database and updates moderators. - #[tracing::instrument(skip_all)] async fn from_json(group: Group, context: &Data) -> LemmyResult { + let local_site = LocalSite::read(&mut context.pool()).await.ok(); let instance_id = fetch_instance_actor_for_object(&group.id, context).await?; - let local_site = LocalSite::read(&mut context.pool()).await.ok(); - let slur_regex = &local_site_opt_to_slur_regex(&local_site); + let slur_regex = slur_regex(context).await?; let url_blocklist = get_url_blocklist(context).await?; let sidebar = read_from_string_or_source_opt(&group.content, &None, &group.source); - let sidebar = process_markdown_opt(&sidebar, slur_regex, &url_blocklist, context).await?; + let sidebar = process_markdown_opt(&sidebar, &slur_regex, &url_blocklist, context).await?; let sidebar = markdown_rewrite_remote_links_opt(sidebar, context).await; let icon = proxy_image_link_opt_apub(group.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(group.image.map(|i| i.url), context).await?; @@ -158,17 +151,24 @@ impl Object for ApubCommunity { } else { CommunityVisibility::Public }); + + // If NSFW is not allowed, then remove NSFW communities + let removed = check_nsfw_allowed(group.sensitive, local_site.as_ref()) + .err() + .map(|_| true); + let form = CommunityInsertForm { published: group.published, updated: group.updated, deleted: Some(false), nsfw: Some(group.sensitive.unwrap_or(false)), - actor_id: Some(group.id.into()), + ap_id: Some(group.id.into()), local: Some(false), last_refreshed_at: Some(Utc::now()), icon, banner, sidebar, + removed, description: group.summary, followers_url: group.followers.clone().map(Into::into), inbox_url: Some( @@ -193,16 +193,10 @@ impl Object for ApubCommunity { LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).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(); + let community = Community::insert_apub(&mut context.pool(), timestamp, &form).await?; CommunityLanguage::update(&mut context.pool(), languages, community.id).await?; - // 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(); - } + let community: ApubCommunity = community.into(); // These collections are not necessary for Lemmy to work, so ignore errors. let community_ = community.clone(); @@ -215,6 +209,9 @@ 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(()) }); @@ -224,7 +221,7 @@ impl Object for ApubCommunity { impl Actor for ApubCommunity { fn id(&self) -> Url { - self.actor_id.inner().clone() + self.ap_id.inner().clone() } fn public_key_pem(&self) -> &str { @@ -250,27 +247,6 @@ impl GetActorType for ApubCommunity { } } -impl ApubCommunity { - /// For a given community, returns the inboxes of all followers. - #[tracing::instrument(skip_all)] - pub(crate) async fn get_follower_inboxes(&self, context: &LemmyContext) -> LemmyResult> { - let id = self.id; - - let local_site_data = local_site_data_cached(&mut context.pool()).await?; - let follows = - CommunityFollowerView::get_community_follower_inboxes(&mut context.pool(), id).await?; - let inboxes: Vec = follows - .into_iter() - .map(Into::into) - .filter(|inbox: &Url| inbox.host_str() != Some(&context.settings().hostname)) - // Don't send to blocked instances - .filter(|inbox| check_apub_id_valid(inbox, &local_site_data).is_ok()) - .collect(); - - Ok(inboxes) - } -} - #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index 754172fe2..9a773a9b5 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -1,9 +1,7 @@ -use super::verify_is_remote_object; use crate::{ activities::GetActorType, check_apub_id_valid_with_strictness, fetcher::markdown_links::markdown_rewrite_remote_links_opt, - local_site_data_cached, objects::read_from_string_or_source_opt, protocol::{ objects::{instance::Instance, LanguageTag}, @@ -15,18 +13,16 @@ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, kinds::actor::ApplicationType, - protocol::{values::MediaTypeHtml, verification::verify_domains_match}, + protocol::{ + values::MediaTypeHtml, + verification::{verify_domains_match, verify_is_remote_object}, + }, traits::{Actor, Object}, }; use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, - utils::{ - get_url_blocklist, - local_site_opt_to_slur_regex, - process_markdown_opt, - proxy_image_link_opt_apub, - }, + utils::{get_url_blocklist, process_markdown_opt, proxy_image_link_opt_apub, slur_regex}, }; use lemmy_db_schema::{ newtypes::InstanceId, @@ -35,7 +31,6 @@ use lemmy_db_schema::{ activity::ActorType, actor_language::SiteLanguage, instance::Instance as DbInstance, - local_site::LocalSite, site::{Site, SiteInsertForm}, }, traits::Crud, @@ -77,7 +72,6 @@ impl Object for ApubSite { Some(self.last_refreshed_at) } - #[tracing::instrument(skip_all)] async fn read_from_id(object_id: Url, data: &Data) -> LemmyResult> { Ok( Site::read_from_apub_id(&mut data.pool(), &object_id.into()) @@ -90,7 +84,6 @@ impl Object for ApubSite { Err(FederationError::CantDeleteSite.into()) } - #[tracing::instrument(skip_all)] async fn into_json(self, data: &Data) -> LemmyResult { let site_id = self.id; let langs = SiteLanguage::read(&mut data.pool(), site_id).await?; @@ -108,7 +101,7 @@ impl Object for ApubSite { icon: self.icon.clone().map(ImageObject::new), image: self.banner.clone().map(ImageObject::new), inbox: self.inbox_url.clone().into(), - outbox: Url::parse(&format!("{}site_outbox", self.actor_id))?, + outbox: Url::parse(&format!("{}site_outbox", self.ap_id))?, public_key: self.public_key(), language, content_warning: self.content_warning.clone(), @@ -118,7 +111,6 @@ impl Object for ApubSite { Ok(instance) } - #[tracing::instrument(skip_all)] async fn verify( apub: &Self::Kind, expected_domain: &Url, @@ -128,15 +120,13 @@ impl Object for ApubSite { verify_domains_match(expected_domain, apub.id.inner())?; verify_is_remote_object(&apub.id, data)?; - let local_site_data = local_site_data_cached(&mut data.pool()).await?; - let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site); + let slur_regex = &slur_regex(data).await?; check_slurs(&apub.name, slur_regex)?; check_slurs_opt(&apub.summary, slur_regex)?; Ok(()) } - #[tracing::instrument(skip_all)] async fn from_json(apub: Self::Kind, context: &Data) -> LemmyResult { let domain = apub .id @@ -145,11 +135,10 @@ impl Object for ApubSite { .ok_or(FederationError::UrlWithoutDomain)?; let instance = DbInstance::read_or_create(&mut context.pool(), domain.to_string()).await?; - let local_site = LocalSite::read(&mut context.pool()).await.ok(); - let slur_regex = &local_site_opt_to_slur_regex(&local_site); + let slur_regex = slur_regex(context).await?; let url_blocklist = get_url_blocklist(context).await?; let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.source); - let sidebar = process_markdown_opt(&sidebar, slur_regex, &url_blocklist, context).await?; + let sidebar = process_markdown_opt(&sidebar, &slur_regex, &url_blocklist, context).await?; let sidebar = markdown_rewrite_remote_links_opt(sidebar, context).await; let icon = proxy_image_link_opt_apub(apub.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(apub.image.map(|i| i.url), context).await?; @@ -161,7 +150,7 @@ impl Object for ApubSite { icon, banner, description: apub.summary, - actor_id: Some(apub.id.clone().into()), + ap_id: Some(apub.id.clone().into()), last_refreshed_at: Some(Utc::now()), inbox_url: Some(apub.inbox.clone().into()), public_key: Some(apub.public_key.public_key_pem.clone()), @@ -180,7 +169,7 @@ impl Object for ApubSite { impl Actor for ApubSite { fn id(&self) -> Url { - self.actor_id.inner().clone() + self.ap_id.inner().clone() } fn public_key_pem(&self) -> &str { @@ -207,7 +196,7 @@ pub(in crate::objects) async fn fetch_instance_actor_for_object + C context: &Data, ) -> LemmyResult { let object_id: Url = object_id.clone().into(); - let instance_id = Site::instance_actor_id_from_url(object_id); + let instance_id = Site::instance_ap_id_from_url(object_id); let site = ObjectId::::from(instance_id.clone()) .dereference(context) .await; diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index f837f7ad3..b679636a3 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -1,16 +1,8 @@ use crate::protocol::{objects::page::Attachment, Source}; -use activitypub_federation::{ - config::Data, - fetch::object_id::ObjectId, - protocol::values::MediaTypeMarkdownOrHtml, - traits::Object, -}; -use anyhow::anyhow; +use activitypub_federation::{config::Data, protocol::values::MediaTypeMarkdownOrHtml}; use html2md::parse_html; use lemmy_api_common::context::LemmyContext; use lemmy_utils::error::LemmyResult; -use serde::Deserialize; -use std::fmt::Debug; pub mod comment; pub mod community; @@ -62,22 +54,3 @@ pub(crate) async fn append_attachments_to_comment( Ok(content) } - -/// When for example a Post is made in a remote community, the community will send it back, -/// wrapped in Announce. If we simply receive this like any other federated object, overwrite the -/// existing, local Post. In particular, it will set the field local = false, so that the object -/// can't be fetched from the Activitypub HTTP endpoint anymore (which only serves local objects). -pub(crate) fn verify_is_remote_object( - id: &ObjectId, - context: &Data, -) -> LemmyResult<()> -where - T: Object + Debug + Send + 'static, - for<'de2> ::Kind: Deserialize<'de2>, -{ - if id.is_local(context) { - Err(anyhow!("cant accept local object from remote instance").into()) - } else { - Ok(()) - } -} diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 50f8e8563..6ca9d6f6f 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -1,9 +1,7 @@ -use super::verify_is_remote_object; use crate::{ activities::GetActorType, check_apub_id_valid_with_strictness, fetcher::markdown_links::markdown_rewrite_remote_links_opt, - local_site_data_cached, objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt}, protocol::{ objects::person::{Person, UserTypes}, @@ -13,7 +11,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - protocol::verification::verify_domains_match, + protocol::verification::{verify_domains_match, verify_is_remote_object}, traits::{Actor, Object}, }; use chrono::{DateTime, Utc}; @@ -22,16 +20,15 @@ use lemmy_api_common::{ utils::{ generate_outbox_url, get_url_blocklist, - local_site_opt_to_slur_regex, process_markdown_opt, proxy_image_link_opt_apub, + slur_regex, }, }; use lemmy_db_schema::{ sensitive::SensitiveString, source::{ activity::ActorType, - local_site::LocalSite, person::{Person as DbPerson, PersonInsertForm, PersonUpdateForm}, }, traits::{ApubActor, Crud}, @@ -72,7 +69,6 @@ impl Object for ApubPerson { Some(self.last_refreshed_at) } - #[tracing::instrument(skip_all)] async fn read_from_id( object_id: Url, context: &Data, @@ -84,7 +80,6 @@ impl Object for ApubPerson { ) } - #[tracing::instrument(skip_all)] async fn delete(self, context: &Data) -> LemmyResult<()> { let form = PersonUpdateForm { deleted: Some(true), @@ -94,7 +89,6 @@ impl Object for ApubPerson { Ok(()) } - #[tracing::instrument(skip_all)] async fn into_json(self, _context: &Data) -> LemmyResult { let kind = if self.bot_account { UserTypes::Service @@ -104,7 +98,7 @@ impl Object for ApubPerson { let person = Person { kind, - id: self.actor_id.clone().into(), + id: self.ap_id.clone().into(), preferred_username: self.name.clone(), name: self.display_name.clone(), summary: self.bio.as_ref().map(|b| markdown_to_html(b)), @@ -113,7 +107,7 @@ impl Object for ApubPerson { image: self.banner.clone().map(ImageObject::new), matrix_user_id: self.matrix_user_id.clone(), published: Some(self.published), - outbox: generate_outbox_url(&self.actor_id)?.into(), + outbox: generate_outbox_url(&self.ap_id)?.into(), endpoints: None, public_key: self.public_key(), updated: self.updated, @@ -122,35 +116,31 @@ impl Object for ApubPerson { Ok(person) } - #[tracing::instrument(skip_all)] async fn verify( person: &Person, expected_domain: &Url, context: &Data, ) -> LemmyResult<()> { - let local_site_data = local_site_data_cached(&mut context.pool()).await?; - let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site); - check_slurs(&person.preferred_username, slur_regex)?; - check_slurs_opt(&person.name, slur_regex)?; + let slur_regex = slur_regex(context).await?; + check_slurs(&person.preferred_username, &slur_regex)?; + check_slurs_opt(&person.name, &slur_regex)?; verify_domains_match(person.id.inner(), expected_domain)?; verify_is_remote_object(&person.id, context)?; check_apub_id_valid_with_strictness(person.id.inner(), false, context).await?; let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source); - check_slurs_opt(&bio, slur_regex)?; + check_slurs_opt(&bio, &slur_regex)?; Ok(()) } - #[tracing::instrument(skip_all)] async fn from_json(person: Person, context: &Data) -> LemmyResult { let instance_id = fetch_instance_actor_for_object(&person.id, context).await?; - let local_site = LocalSite::read(&mut context.pool()).await.ok(); - let slur_regex = &local_site_opt_to_slur_regex(&local_site); + let slur_regex = slur_regex(context).await?; let url_blocklist = get_url_blocklist(context).await?; let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source); - let bio = process_markdown_opt(&bio, slur_regex, &url_blocklist, context).await?; + let bio = process_markdown_opt(&bio, &slur_regex, &url_blocklist, context).await?; let bio = markdown_rewrite_remote_links_opt(bio, context).await; let avatar = proxy_image_link_opt_apub(person.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(person.image.map(|i| i.url), context).await?; @@ -169,7 +159,7 @@ impl Object for ApubPerson { banner, published: person.published, updated: person.updated, - actor_id: Some(person.id.into()), + ap_id: Some(person.id.into()), bio, local: Some(false), bot_account: Some(person.kind == UserTypes::Service), @@ -194,7 +184,7 @@ impl Object for ApubPerson { impl Actor for ApubPerson { fn id(&self) -> Url { - self.actor_id.inner().clone() + self.ap_id.inner().clone() } fn public_key_pem(&self) -> &str { @@ -274,7 +264,7 @@ pub(crate) mod tests { ApubPerson::verify(&json, &url, &context).await?; let person = ApubPerson::from_json(json, &context).await?; - assert_eq!(person.actor_id, url.into()); + assert_eq!(person.ap_id, url.into()); assert_eq!(person.name, "lanodan"); assert!(!person.local); assert_eq!(context.request_count(), 0); diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 73e940a3c..9422ef459 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -2,8 +2,7 @@ use crate::{ activities::{generate_to, verify_person_in_community, verify_visibility}, check_apub_id_valid_with_strictness, fetcher::markdown_links::{markdown_rewrite_remote_links_opt, to_local_url}, - local_site_data_cached, - objects::{read_from_string_or_source_opt, verify_is_remote_object}, + objects::read_from_string_or_source_opt, protocol::{ objects::{ page::{Attachment, AttributedTo, Hashtag, HashtagType, Page, PageType}, @@ -16,16 +15,25 @@ use crate::{ }; use activitypub_federation::{ config::Data, - protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match}, + protocol::{ + values::MediaTypeMarkdownOrHtml, + verification::{verify_domains_match, verify_is_remote_object}, + }, traits::Object, }; use anyhow::anyhow; use chrono::{DateTime, Utc}; -use html2text::{from_read_with_decorator, render::text_renderer::TrivialDecorator}; +use html2text::{from_read_with_decorator, render::TrivialDecorator}; use lemmy_api_common::{ context::LemmyContext, request::generate_post_link_metadata, - utils::{get_url_blocklist, local_site_opt_to_slur_regex, process_markdown_opt}, + utils::{ + check_nsfw_allowed, + get_url_blocklist, + process_markdown_opt, + purge_post_images, + slur_regex, + }, }; use lemmy_db_schema::{ source::{ @@ -36,7 +44,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views_actor::structs::CommunityModeratorView; +use lemmy_db_views::structs::CommunityModeratorView; use lemmy_utils::{ error::{LemmyError, LemmyResult}, spawn_try_task, @@ -78,7 +86,6 @@ impl Object for ApubPost { None } - #[tracing::instrument(skip_all)] async fn read_from_id( object_id: Url, context: &Data, @@ -90,7 +97,6 @@ impl Object for ApubPost { ) } - #[tracing::instrument(skip_all)] async fn delete(self, context: &Data) -> LemmyResult<()> { if !self.deleted { let form = PostUpdateForm { @@ -103,7 +109,7 @@ impl Object for ApubPost { } // Turn a Lemmy post into an ActivityPub page that can be sent out over the network. - #[tracing::instrument(skip_all)] + async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; let creator = Person::read(&mut context.pool(), creator_id).await?; @@ -132,7 +138,7 @@ impl Object for ApubPost { let page = Page { kind: PageType::Page, id: self.ap_id.clone().into(), - attributed_to: AttributedTo::Lemmy(creator.actor_id.into()), + attributed_to: AttributedTo::Lemmy(creator.ap_id.into()), to: generate_to(&community)?, cc: vec![], name: Some(self.name.clone()), @@ -151,7 +157,6 @@ impl Object for ApubPost { Ok(page) } - #[tracing::instrument(skip_all)] async fn verify( page: &Page, expected_domain: &Url, @@ -164,20 +169,23 @@ impl Object for ApubPost { check_apub_id_valid_with_strictness(page.id.inner(), community.local, context).await?; verify_person_in_community(&page.creator()?, &community, context).await?; - let local_site_data = local_site_data_cached(&mut context.pool()).await?; - let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site); - check_slurs_opt(&page.name, slur_regex)?; + let slur_regex = slur_regex(context).await?; + check_slurs_opt(&page.name, &slur_regex)?; verify_domains_match(page.creator()?.inner(), page.id.inner())?; verify_visibility(&page.to, &page.cc, &community)?; Ok(()) } - #[tracing::instrument(skip_all)] async fn from_json(page: Page, context: &Data) -> LemmyResult { + let local_site = LocalSite::read(&mut context.pool()).await.ok(); let creator = page.creator()?.dereference(context).await?; let community = page.community(context).await?; - if community.posting_restricted_to_mods { + + // Prevent posts from non-mod users in local, restricted community. If its a remote community + // then its possible that the restricted setting was enabled recently, so existing user posts + // should still be fetched. + if community.local && community.posting_restricted_to_mods { CommunityModeratorView::check_is_community_moderator( &mut context.pool(), community.id, @@ -198,7 +206,7 @@ impl Object for ApubPost { .map(StringReader::new) .map(|c| from_read_with_decorator(c, MAX_TITLE_LENGTH, TrivialDecorator::new())) .and_then(|c| { - c.lines().next().map(|s| { + c.unwrap_or_default().lines().next().map(|s| { s.replace(&format!("@{}", community.name), "") .trim() .to_string() @@ -211,8 +219,6 @@ impl Object for ApubPost { } let first_attachment = page.attachment.first(); - let local_site = LocalSite::read(&mut context.pool()).await.ok(); - let url = if let Some(attachment) = first_attachment.cloned() { Some(attachment.url()) } else if page.kind == PageType::Video { @@ -222,6 +228,17 @@ impl Object for ApubPost { None }; + // If NSFW is not allowed, reject NSFW posts and delete existing + // posts that get updated to be NSFW + let block_for_nsfw = check_nsfw_allowed(page.sensitive, local_site.as_ref()); + if let Err(e) = block_for_nsfw { + let url = url.clone().map(std::convert::Into::into); + let thumbnail_url = page.image.map(|i| i.url.into()); + purge_post_images(url, thumbnail_url, context).await; + Post::delete_from_apub_id(&mut context.pool(), page.id.inner().clone()).await?; + Err(e)? + } + let url_blocklist = get_url_blocklist(context).await?; let url = if let Some(url) = url { @@ -234,10 +251,10 @@ impl Object for ApubPost { let alt_text = first_attachment.cloned().and_then(Attachment::alt_text); - let slur_regex = &local_site_opt_to_slur_regex(&local_site); + let slur_regex = slur_regex(context).await?; let body = read_from_string_or_source_opt(&page.content, &page.media_type, &page.source); - let body = process_markdown_opt(&body, slur_regex, &url_blocklist, context).await?; + let body = process_markdown_opt(&body, &slur_regex, &url_blocklist, context).await?; let body = markdown_rewrite_remote_links_opt(body, context).await; let language_id = Some( LanguageTag::to_language_id_single(page.language.unwrap_or_default(), &mut context.pool()) diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 521419c82..cfc60e041 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -1,4 +1,3 @@ -use super::verify_is_remote_object; use crate::{ check_apub_id_valid_with_strictness, fetcher::markdown_links::markdown_rewrite_remote_links, @@ -10,23 +9,20 @@ use crate::{ }; use activitypub_federation::{ config::Data, - protocol::{values::MediaTypeHtml, verification::verify_domains_match}, + protocol::{ + values::MediaTypeHtml, + verification::{verify_domains_match, verify_is_remote_object}, + }, traits::Object, }; use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, - utils::{ - check_private_messages_enabled, - get_url_blocklist, - local_site_opt_to_slur_regex, - process_markdown, - }, + utils::{check_private_messages_enabled, get_url_blocklist, process_markdown, slur_regex}, }; use lemmy_db_schema::{ source::{ instance::Instance, - local_site::LocalSite, person::Person, person_block::PersonBlock, private_message::{PrivateMessage as DbPrivateMessage, PrivateMessageInsertForm}, @@ -68,7 +64,6 @@ impl Object for ApubPrivateMessage { None } - #[tracing::instrument(skip_all)] async fn read_from_id( object_id: Url, context: &Data, @@ -85,7 +80,6 @@ impl Object for ApubPrivateMessage { Err(LemmyErrorType::NotFound.into()) } - #[tracing::instrument(skip_all)] async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; let creator = Person::read(&mut context.pool(), creator_id).await?; @@ -107,8 +101,8 @@ impl Object for ApubPrivateMessage { let note = PrivateMessage { kind, id: self.ap_id.clone().into(), - attributed_to: creator.actor_id.into(), - to: [recipient.actor_id.into()], + attributed_to: creator.ap_id.into(), + to: [recipient.ap_id.into()], content: markdown_to_html(&self.content), media_type: Some(MediaTypeHtml::Html), source: Some(Source::new(self.content.clone())), @@ -118,7 +112,6 @@ impl Object for ApubPrivateMessage { Ok(note) } - #[tracing::instrument(skip_all)] async fn verify( note: &PrivateMessage, expected_domain: &Url, @@ -132,14 +125,13 @@ impl Object for ApubPrivateMessage { let person = note.attributed_to.dereference(context).await?; if person.banned { Err(FederationError::PersonIsBannedFromSite( - person.actor_id.to_string(), + person.ap_id.to_string(), ))? } else { Ok(()) } } - #[tracing::instrument(skip_all)] async fn from_json( note: PrivateMessage, context: &Data, @@ -154,12 +146,11 @@ impl Object for ApubPrivateMessage { { check_private_messages_enabled(&recipient_local_user)?; } - let local_site = LocalSite::read(&mut context.pool()).await.ok(); - let slur_regex = &local_site_opt_to_slur_regex(&local_site); + let slur_regex = slur_regex(context).await?; let url_blocklist = get_url_blocklist(context).await?; let content = read_from_string_or_source(¬e.content, &None, ¬e.source); - let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?; + let content = process_markdown(&content, &slur_regex, &url_blocklist, context).await?; let content = markdown_rewrite_remote_links(content, context).await; let form = PrivateMessageInsertForm { diff --git a/crates/apub/src/protocol/activities/block/block_user.rs b/crates/apub/src/protocol/activities/block/block_user.rs index 97f54a2a3..f0b3c817f 100644 --- a/crates/apub/src/protocol/activities/block/block_user.rs +++ b/crates/apub/src/protocol/activities/block/block_user.rs @@ -40,7 +40,6 @@ pub struct BlockUser { pub(crate) end_time: Option>, } -#[async_trait::async_trait] impl InCommunity for BlockUser { async fn community(&self, context: &Data) -> LemmyResult { let target = self.target.dereference(context).await?; diff --git a/crates/apub/src/protocol/activities/block/undo_block_user.rs b/crates/apub/src/protocol/activities/block/undo_block_user.rs index 1dad9b03c..c5dcebb15 100644 --- a/crates/apub/src/protocol/activities/block/undo_block_user.rs +++ b/crates/apub/src/protocol/activities/block/undo_block_user.rs @@ -33,7 +33,6 @@ pub struct UndoBlockUser { pub(crate) restore_data: Option, } -#[async_trait::async_trait] impl InCommunity for UndoBlockUser { async fn community(&self, context: &Data) -> LemmyResult { let community = self.object.community(context).await?; diff --git a/crates/apub/src/protocol/activities/community/collection_add.rs b/crates/apub/src/protocol/activities/community/collection_add.rs index d7c5e5143..13a713fd1 100644 --- a/crates/apub/src/protocol/activities/community/collection_add.rs +++ b/crates/apub/src/protocol/activities/community/collection_add.rs @@ -29,7 +29,6 @@ pub struct CollectionAdd { pub(crate) id: Url, } -#[async_trait::async_trait] impl InCommunity for CollectionAdd { async fn community(&self, context: &Data) -> LemmyResult { let (community, _) = diff --git a/crates/apub/src/protocol/activities/community/collection_remove.rs b/crates/apub/src/protocol/activities/community/collection_remove.rs index 85b314b4b..d3370269c 100644 --- a/crates/apub/src/protocol/activities/community/collection_remove.rs +++ b/crates/apub/src/protocol/activities/community/collection_remove.rs @@ -29,7 +29,6 @@ pub struct CollectionRemove { pub(crate) id: Url, } -#[async_trait::async_trait] impl InCommunity for CollectionRemove { async fn community(&self, context: &Data) -> LemmyResult { let (community, _) = diff --git a/crates/apub/src/protocol/activities/community/lock_page.rs b/crates/apub/src/protocol/activities/community/lock_page.rs index a97466edc..237b2290c 100644 --- a/crates/apub/src/protocol/activities/community/lock_page.rs +++ b/crates/apub/src/protocol/activities/community/lock_page.rs @@ -48,7 +48,6 @@ pub struct UndoLockPage { pub(crate) id: Url, } -#[async_trait::async_trait] impl InCommunity for LockPage { async fn community(&self, context: &Data) -> LemmyResult { let post = self.object.dereference(context).await?; @@ -57,7 +56,6 @@ impl InCommunity for LockPage { } } -#[async_trait::async_trait] impl InCommunity for UndoLockPage { async fn community(&self, context: &Data) -> LemmyResult { let community = self.object.community(context).await?; diff --git a/crates/apub/src/protocol/activities/community/mod.rs b/crates/apub/src/protocol/activities/community/mod.rs index 0c52e6e77..ab734fc4f 100644 --- a/crates/apub/src/protocol/activities/community/mod.rs +++ b/crates/apub/src/protocol/activities/community/mod.rs @@ -3,10 +3,12 @@ pub mod collection_add; pub mod collection_remove; pub mod lock_page; pub mod report; +pub mod resolve_report; pub mod update; #[cfg(test)] mod tests { + use super::resolve_report::ResolveReport; use crate::protocol::{ activities::community::{ announce::AnnounceActivity, @@ -44,6 +46,10 @@ mod tests { )?; test_parse_lemmy_item::("assets/lemmy/activities/community/report_page.json")?; + test_parse_lemmy_item::( + "assets/lemmy/activities/community/resolve_report_page.json", + )?; + Ok(()) } } diff --git a/crates/apub/src/protocol/activities/community/report.rs b/crates/apub/src/protocol/activities/community/report.rs index 71ecf4c5c..35b379b93 100644 --- a/crates/apub/src/protocol/activities/community/report.rs +++ b/crates/apub/src/protocol/activities/community/report.rs @@ -49,14 +49,17 @@ pub(crate) enum ReportObject { } impl ReportObject { - pub async fn dereference(self, context: &Data) -> LemmyResult { + pub(crate) async fn dereference( + &self, + context: &Data, + ) -> LemmyResult { match self { ReportObject::Lemmy(l) => l.dereference(context).await, ReportObject::Mastodon(objects) => { for o in objects { // Find the first reported item which can be dereferenced as post or comment (Lemmy can // only handle one item per report). - let deref = ObjectId::from(o).dereference(context).await; + let deref = ObjectId::from(o.clone()).dereference(context).await; if deref.is_ok() { return deref; } @@ -65,9 +68,29 @@ impl ReportObject { } } } + + pub(crate) async fn object_id( + &self, + context: &Data, + ) -> LemmyResult> { + match self { + ReportObject::Lemmy(l) => Ok(l.clone()), + ReportObject::Mastodon(objects) => { + for o in objects { + // Same logic as above, but return the ID and not the object itself. + let deref = ObjectId::::from(o.clone()) + .dereference(context) + .await; + if deref.is_ok() { + return Ok(o.clone().into()); + } + } + Err(LemmyErrorType::NotFound.into()) + } + } + } } -#[async_trait::async_trait] impl InCommunity for Report { async fn community(&self, context: &Data) -> LemmyResult { let community = self.to[0].dereference(context).await?; diff --git a/crates/apub/src/protocol/activities/community/resolve_report.rs b/crates/apub/src/protocol/activities/community/resolve_report.rs new file mode 100644 index 000000000..c15753476 --- /dev/null +++ b/crates/apub/src/protocol/activities/community/resolve_report.rs @@ -0,0 +1,38 @@ +use super::report::Report; +use crate::{ + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::InCommunity, +}; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + protocol::helpers::deserialize_one, +}; +use lemmy_api_common::context::LemmyContext; +use lemmy_utils::error::LemmyResult; +use serde::{Deserialize, Serialize}; +use strum::Display; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display)] +pub enum ResolveType { + Resolve, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolveReport { + pub(crate) actor: ObjectId, + #[serde(deserialize_with = "deserialize_one")] + pub(crate) to: [ObjectId; 1], + pub(crate) object: Report, + #[serde(rename = "type")] + pub(crate) kind: ResolveType, + pub(crate) id: Url, +} + +impl InCommunity for ResolveReport { + async fn community(&self, context: &Data) -> LemmyResult { + self.object.community(context).await + } +} diff --git a/crates/apub/src/protocol/activities/community/update.rs b/crates/apub/src/protocol/activities/community/update.rs index 080dd27e6..0fee46fff 100644 --- a/crates/apub/src/protocol/activities/community/update.rs +++ b/crates/apub/src/protocol/activities/community/update.rs @@ -30,7 +30,6 @@ pub struct UpdateCommunity { pub(crate) id: Url, } -#[async_trait::async_trait] impl InCommunity for UpdateCommunity { async fn community(&self, context: &Data) -> LemmyResult { let community: ApubCommunity = self.object.id.clone().dereference(context).await?; diff --git a/crates/apub/src/protocol/activities/create_or_update/note.rs b/crates/apub/src/protocol/activities/create_or_update/note.rs index 37f595337..9b5027f81 100644 --- a/crates/apub/src/protocol/activities/create_or_update/note.rs +++ b/crates/apub/src/protocol/activities/create_or_update/note.rs @@ -30,7 +30,6 @@ pub struct CreateOrUpdateNote { pub(crate) id: Url, } -#[async_trait::async_trait] impl InCommunity for CreateOrUpdateNote { async fn community(&self, context: &Data) -> LemmyResult { let post = self.object.get_parents(context).await?.0; diff --git a/crates/apub/src/protocol/activities/create_or_update/page.rs b/crates/apub/src/protocol/activities/create_or_update/page.rs index 3fd397022..b300ac585 100644 --- a/crates/apub/src/protocol/activities/create_or_update/page.rs +++ b/crates/apub/src/protocol/activities/create_or_update/page.rs @@ -26,7 +26,6 @@ pub struct CreateOrUpdatePage { pub(crate) id: Url, } -#[async_trait::async_trait] impl InCommunity for CreateOrUpdatePage { async fn community(&self, context: &Data) -> LemmyResult { let community = self.object.community(context).await?; diff --git a/crates/apub/src/protocol/activities/deletion/delete.rs b/crates/apub/src/protocol/activities/deletion/delete.rs index 878f6fb6c..8e50f4efe 100644 --- a/crates/apub/src/protocol/activities/deletion/delete.rs +++ b/crates/apub/src/protocol/activities/deletion/delete.rs @@ -44,7 +44,6 @@ pub struct Delete { pub(crate) remove_data: Option, } -#[async_trait::async_trait] impl InCommunity for Delete { async fn community(&self, context: &Data) -> LemmyResult { let community_id = match DeletableObjects::read_from_db(self.object.id(), context).await? { diff --git a/crates/apub/src/protocol/activities/deletion/undo_delete.rs b/crates/apub/src/protocol/activities/deletion/undo_delete.rs index a6bf0589b..1756f14fb 100644 --- a/crates/apub/src/protocol/activities/deletion/undo_delete.rs +++ b/crates/apub/src/protocol/activities/deletion/undo_delete.rs @@ -31,7 +31,6 @@ pub struct UndoDelete { pub(crate) cc: Vec, } -#[async_trait::async_trait] impl InCommunity for UndoDelete { async fn community(&self, context: &Data) -> LemmyResult { let community = self.object.community(context).await?; diff --git a/crates/apub/src/protocol/activities/voting/undo_vote.rs b/crates/apub/src/protocol/activities/voting/undo_vote.rs index a9f0d0541..73bebee76 100644 --- a/crates/apub/src/protocol/activities/voting/undo_vote.rs +++ b/crates/apub/src/protocol/activities/voting/undo_vote.rs @@ -18,7 +18,6 @@ pub struct UndoVote { pub(crate) id: Url, } -#[async_trait::async_trait] impl InCommunity for UndoVote { async fn community(&self, context: &Data) -> LemmyResult { let community = self.object.community(context).await?; diff --git a/crates/apub/src/protocol/activities/voting/vote.rs b/crates/apub/src/protocol/activities/voting/vote.rs index b6ffe485a..a4744aa58 100644 --- a/crates/apub/src/protocol/activities/voting/vote.rs +++ b/crates/apub/src/protocol/activities/voting/vote.rs @@ -47,7 +47,6 @@ impl From<&VoteType> for i16 { } } -#[async_trait::async_trait] impl InCommunity for Vote { async fn community(&self, context: &Data) -> LemmyResult { let community = self diff --git a/crates/apub/src/protocol/mod.rs b/crates/apub/src/protocol/mod.rs index 90f1f51ea..23119fe3f 100644 --- a/crates/apub/src/protocol/mod.rs +++ b/crates/apub/src/protocol/mod.rs @@ -9,7 +9,7 @@ use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::newtypes::DbUrl; use lemmy_utils::error::LemmyResult; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::collections::HashMap; +use std::{collections::HashMap, future::Future}; use url::Url; pub mod activities; @@ -80,9 +80,11 @@ impl IdOrNestedObject { } } -#[async_trait::async_trait] pub trait InCommunity { - async fn community(&self, context: &Data) -> LemmyResult; + fn community( + &self, + context: &Data, + ) -> impl Future> + Send; } #[cfg(test)] diff --git a/crates/apub/src/protocol/objects/group.rs b/crates/apub/src/protocol/objects/group.rs index dbf4af892..cd0a6f146 100644 --- a/crates/apub/src/protocol/objects/group.rs +++ b/crates/apub/src/protocol/objects/group.rs @@ -6,7 +6,6 @@ use crate::{ community_moderators::ApubCommunityModerators, community_outbox::ApubCommunityOutbox, }, - local_site_data_cached, objects::community::ApubCommunity, protocol::{ objects::{Endpoints, LanguageTag}, @@ -26,7 +25,7 @@ use activitypub_federation::{ }, }; use chrono::{DateTime, Utc}; -use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex}; +use lemmy_api_common::{context::LemmyContext, utils::slur_regex}; use lemmy_utils::{ error::LemmyResult, utils::slurs::{check_slurs, check_slurs_opt}, @@ -61,6 +60,7 @@ pub struct Group { #[serde(deserialize_with = "deserialize_skip_error", default)] pub(crate) icon: Option, /// banner + #[serde(deserialize_with = "deserialize_skip_error", default)] pub(crate) image: Option, // lemmy extension pub(crate) sensitive: Option, @@ -88,12 +88,11 @@ impl Group { check_apub_id_valid_with_strictness(self.id.inner(), true, context).await?; verify_domains_match(expected_domain, self.id.inner())?; - let local_site_data = local_site_data_cached(&mut context.pool()).await?; - let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site); + let slur_regex = slur_regex(context).await?; - check_slurs(&self.preferred_username, slur_regex)?; - check_slurs_opt(&self.name, slur_regex)?; - check_slurs_opt(&self.summary, slur_regex)?; + check_slurs(&self.preferred_username, &slur_regex)?; + check_slurs_opt(&self.name, &slur_regex)?; + check_slurs_opt(&self.summary, &slur_regex)?; Ok(()) } } diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs index d4c87333f..5fdd1123e 100644 --- a/crates/apub/src/protocol/objects/note.rs +++ b/crates/apub/src/protocol/objects/note.rs @@ -87,7 +87,6 @@ impl Note { } } -#[async_trait::async_trait] impl InCommunity for Note { async fn community(&self, context: &Data) -> LemmyResult { let (post, _) = self.get_parents(context).await?; diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs index ae5925e57..d2f6bdb05 100644 --- a/crates/apub/src/protocol/objects/page.rs +++ b/crates/apub/src/protocol/objects/page.rs @@ -226,7 +226,6 @@ impl ActivityHandler for Page { } } -#[async_trait::async_trait] impl InCommunity for Page { async fn community(&self, context: &Data) -> LemmyResult { let community = match &self.attributed_to { diff --git a/crates/db_perf/src/main.rs b/crates/db_perf/src/main.rs index 71554219b..c99c379fb 100644 --- a/crates/db_perf/src/main.rs +++ b/crates/db_perf/src/main.rs @@ -22,7 +22,7 @@ use lemmy_db_schema::{ utils::{build_db_pool, get_conn, now}, PostSortType, }; -use lemmy_db_views::{post_view::PostQuery, structs::PaginationCursor}; +use lemmy_db_views::{post::post_view::PostQuery, structs::PostPaginationCursor}; use lemmy_utils::error::{LemmyErrorExt2, LemmyResult}; use std::num::NonZeroU32; use url::Url; @@ -160,9 +160,9 @@ async fn try_main() -> LemmyResult<()> { .list(&site()?, &mut conn.into()) .await?; - if let Some(post_view) = post_views.into_iter().last() { + if let Some(post_view) = post_views.into_iter().next_back() { println!("👀 getting pagination cursor data for next page"); - let cursor_data = PaginationCursor::after_post(&post_view) + let cursor_data = PostPaginationCursor::after_post(&post_view) .read(&mut conn.into(), None) .await?; page_after = Some(cursor_data); @@ -192,7 +192,7 @@ fn site() -> LemmyResult { icon: None, banner: None, description: None, - actor_id: Url::parse("http://example.com")?.into(), + ap_id: Url::parse("http://example.com")?.into(), last_refreshed_at: Default::default(), inbox_url: Url::parse("http://example.com")?.into(), private_key: None, diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index 093035a1e..b7bdcfc3f 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -38,7 +38,6 @@ full = [ "rustls", "i-love-jesus", "tuplex", - "diesel-bind-if-some", ] [dependencies] @@ -56,6 +55,7 @@ diesel = { workspace = true, features = [ "postgres", "serde_json", "uuid", + "64-column-tables", ], optional = true } diesel-derive-newtype = { workspace = true, optional = true } diesel-derive-enum = { workspace = true, optional = true } @@ -66,9 +66,8 @@ diesel-async = { workspace = true, features = [ ], optional = true } regex = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true } -async-trait = { workspace = true } tracing = { workspace = true } -deadpool = { version = "0.12.1", optional = true, features = ["rt_tokio_1"] } +deadpool = { version = "0.12.2", optional = true, features = ["rt_tokio_1"] } ts-rs = { workspace = true, optional = true } futures-util = { workspace = true } tokio = { workspace = true, optional = true } @@ -78,7 +77,6 @@ rustls = { workspace = true, optional = true } uuid.workspace = true i-love-jesus = { workspace = true, optional = true } anyhow = { workspace = true } -diesel-bind-if-some = { workspace = true, optional = true } derive-new.workspace = true tuplex = { workspace = true, optional = true } diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index c0cbfab34..63fc1bd7a 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -6,7 +6,7 @@ -- before the automatic deletion of the row that references it. This is not a problem for insert or delete. -- -- Triggers that update multiple tables should use this order: person_aggregates, comment_aggregates, --- post_aggregates, community_aggregates, site_aggregates +-- post, community_aggregates, site_aggregates -- * The order matters because the updated rows are locked until the end of the transaction, and statements -- in a trigger don't use separate transactions. This means that updates closer to the beginning cause -- longer locks because the duration of each update extends the durations of the locks caused by previous @@ -19,19 +19,6 @@ -- -- -- Create triggers for both post and comments -CREATE FUNCTION r.creator_id_from_post_aggregates (agg post_aggregates) - RETURNS int IMMUTABLE PARALLEL SAFE RETURN agg.creator_id; - -CREATE FUNCTION r.creator_id_from_comment_aggregates (agg comment_aggregates) - RETURNS int IMMUTABLE PARALLEL SAFE RETURN ( - SELECT - creator_id - FROM - comment - WHERE - comment.id = agg.comment_id LIMIT 1 -); - CREATE PROCEDURE r.post_or_comment (table_name text) LANGUAGE plpgsql AS $a$ @@ -41,7 +28,7 @@ BEGIN CALL r.create_triggers ('thing_actions', $$ BEGIN WITH thing_diff AS ( UPDATE - thing_aggregates AS a + thing AS a SET score = a.score + diff.upvotes - diff.downvotes, upvotes = a.upvotes + diff.upvotes, downvotes = a.downvotes + diff.downvotes, controversy_rank = r.controversy_rank ((a.upvotes + diff.upvotes)::numeric, (a.downvotes + diff.downvotes)::numeric) FROM ( @@ -49,18 +36,18 @@ BEGIN (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 + a.id = diff.thing_id AND (diff.upvotes, diff.downvotes) != (0, 0) RETURNING - r.creator_id_from_thing_aggregates (a.*) AS creator_id, diff.upvotes - diff.downvotes AS score) + a.creator_id AS creator_id, diff.upvotes - diff.downvotes AS score) UPDATE - person_aggregates AS a + person AS a SET thing_score = a.thing_score + diff.score FROM ( SELECT creator_id, sum(score) AS score FROM thing_diff GROUP BY creator_id) AS diff WHERE - a.person_id = diff.creator_id + a.id = diff.creator_id AND diff.score != 0; RETURN NULL; END; @@ -93,23 +80,35 @@ END; CALL r.create_triggers ('comment', $$ BEGIN - UPDATE - person_aggregates AS a - SET - comment_count = a.comment_count + diff.comment_count - FROM ( + -- Prevent infinite recursion + IF ( SELECT - (comment).creator_id, coalesce(sum(count_diff), 0) AS comment_count - FROM select_old_and_new_rows AS old_and_new_rows - WHERE - r.is_counted (comment) - GROUP BY (comment).creator_id) AS diff -WHERE - a.person_id = diff.creator_id - AND diff.comment_count != 0; + count(*) + FROM select_old_and_new_rows AS old_and_new_rows) = 0 THEN + RETURN NULL; + +END IF; UPDATE - comment_aggregates AS a + person AS a +SET + comment_count = a.comment_count + diff.comment_count +FROM ( + SELECT + (comment).creator_id, + coalesce(sum(count_diff), 0) AS comment_count + FROM + select_old_and_new_rows AS old_and_new_rows + WHERE + r.is_counted (comment) + GROUP BY + (comment).creator_id) AS diff +WHERE + a.id = diff.creator_id + AND diff.comment_count != 0; + +UPDATE + comment AS a SET child_count = a.child_count + diff.child_count FROM ( @@ -143,66 +142,43 @@ FROM ( GROUP BY parent_id) AS diff WHERE - a.comment_id = diff.parent_id + a.id = diff.parent_id AND diff.child_count != 0; -WITH post_diff AS ( - UPDATE - post_aggregates AS a - SET - comments = a.comments + diff.comments, - newest_comment_time = GREATEST (a.newest_comment_time, diff.newest_comment_time), - newest_comment_time_necro = GREATEST (a.newest_comment_time_necro, diff.newest_comment_time_necro) - FROM ( - SELECT - post.id AS post_id, - coalesce(sum(count_diff), 0) AS comments, - -- Old rows are excluded using `count_diff = 1` - max((comment).published) FILTER (WHERE count_diff = 1) AS newest_comment_time, - max((comment).published) FILTER (WHERE count_diff = 1 - -- Ignore comments from the post's creator - AND post.creator_id != (comment).creator_id - -- Ignore comments on old posts - AND post.published > ((comment).published - '2 days'::interval)) AS newest_comment_time_necro, - r.is_counted (post.*) AS include_in_community_aggregates - FROM - select_old_and_new_rows AS old_and_new_rows - LEFT JOIN post ON post.id = (comment).post_id - WHERE - r.is_counted (comment) - GROUP BY - post.id) AS diff - WHERE - a.post_id = diff.post_id - AND (diff.comments, - GREATEST (a.newest_comment_time, diff.newest_comment_time), - GREATEST (a.newest_comment_time_necro, diff.newest_comment_time_necro)) != (0, - a.newest_comment_time, - a.newest_comment_time_necro) - RETURNING - a.community_id, - diff.comments, - diff.include_in_community_aggregates) UPDATE - community_aggregates AS a + post AS a SET - comments = a.comments + diff.comments + comments = a.comments + diff.comments, + newest_comment_time = GREATEST (a.newest_comment_time, diff.newest_comment_time), + newest_comment_time_necro = GREATEST (a.newest_comment_time_necro, diff.newest_comment_time_necro) FROM ( SELECT - community_id, - sum(comments) AS comments - FROM - post_diff - WHERE - post_diff.include_in_community_aggregates - GROUP BY - community_id) AS diff + post.id AS post_id, + coalesce(sum(count_diff), 0) AS comments, + -- Old rows are excluded using `count_diff = 1` + max((comment).published) FILTER (WHERE count_diff = 1) AS newest_comment_time, + max((comment).published) FILTER (WHERE count_diff = 1 + -- Ignore comments from the post's creator + AND post.creator_id != (comment).creator_id + -- Ignore comments on old posts + AND post.published > ((comment).published - '2 days'::interval)) AS newest_comment_time_necro +FROM + select_old_and_new_rows AS old_and_new_rows + LEFT JOIN post ON post.id = (comment).post_id WHERE - a.community_id = diff.community_id - AND diff.comments != 0; + r.is_counted (comment) +GROUP BY + post.id) AS diff +WHERE + a.id = diff.post_id + AND (diff.comments, + GREATEST (a.newest_comment_time, diff.newest_comment_time), + GREATEST (a.newest_comment_time_necro, diff.newest_comment_time_necro)) != (0, + a.newest_comment_time, + a.newest_comment_time_necro); UPDATE - site_aggregates AS a + local_site AS a SET comments = a.comments + diff.comments FROM ( @@ -225,7 +201,7 @@ $$); CALL r.create_triggers ('post', $$ BEGIN UPDATE - person_aggregates AS a + person AS a SET post_count = a.post_count + diff.post_count FROM ( @@ -236,17 +212,19 @@ BEGIN r.is_counted (post) GROUP BY (post).creator_id) AS diff WHERE - a.person_id = diff.creator_id + a.id = diff.creator_id AND diff.post_count != 0; UPDATE - community_aggregates AS a + community AS a SET - posts = a.posts + diff.posts + posts = a.posts + diff.posts, + comments = a.comments + diff.comments FROM ( SELECT (post).community_id, - coalesce(sum(count_diff), 0) AS posts + coalesce(sum(count_diff), 0) AS posts, + coalesce(sum(count_diff * (post).comments), 0) AS comments FROM select_old_and_new_rows AS old_and_new_rows WHERE @@ -254,11 +232,13 @@ FROM ( GROUP BY (post).community_id) AS diff WHERE - a.community_id = diff.community_id - AND diff.posts != 0; + a.id = diff.community_id + AND (diff.posts, + diff.comments) != (0, + 0); UPDATE - site_aggregates AS a + local_site AS a SET posts = a.posts + diff.posts FROM ( @@ -281,7 +261,7 @@ $$); CALL r.create_triggers ('community', $$ BEGIN UPDATE - site_aggregates AS a + local_site AS a SET communities = a.communities + diff.communities FROM ( @@ -303,7 +283,7 @@ $$); CALL r.create_triggers ('person', $$ BEGIN UPDATE - site_aggregates AS a + local_site AS a SET users = a.users + diff.users FROM ( @@ -320,51 +300,13 @@ END; $$); --- For community_aggregates.comments, don't include comments of deleted or removed posts -CREATE FUNCTION r.update_comment_count_from_post () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - UPDATE - community_aggregates AS a - SET - comments = a.comments + diff.comments - FROM ( - SELECT - old_post.community_id, - sum(( - CASE WHEN r.is_counted (new_post.*) THEN - 1 - ELSE - -1 - END) * post_aggregates.comments) AS comments - FROM - new_post - INNER JOIN old_post ON new_post.id = old_post.id - AND (r.is_counted (new_post.*) != r.is_counted (old_post.*)) - INNER JOIN post_aggregates ON post_aggregates.post_id = new_post.id - GROUP BY - old_post.community_id) AS diff -WHERE - a.community_id = diff.community_id - AND diff.comments != 0; - RETURN NULL; -END; -$$; - -CREATE TRIGGER comment_count - AFTER UPDATE ON post REFERENCING OLD TABLE AS old_post NEW TABLE AS new_post - FOR EACH STATEMENT - EXECUTE FUNCTION r.update_comment_count_from_post (); - -- Count subscribers for communities. -- subscribers should be updated only when a local community is followed by a local or remote person. -- subscribers_local should be updated only when a local person follows a local or remote community. CALL r.create_triggers ('community_actions', $$ BEGIN UPDATE - community_aggregates AS a + community AS a SET subscribers = a.subscribers + diff.subscribers, subscribers_local = a.subscribers_local + diff.subscribers_local FROM ( @@ -375,7 +317,7 @@ BEGIN 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 + a.id = diff.community_id AND (diff.subscribers, diff.subscribers_local) != (0, 0); RETURN NULL; @@ -387,15 +329,16 @@ $$); CALL r.create_triggers ('post_report', $$ BEGIN UPDATE - post_aggregates AS a + post 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 + (post_report).post_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (post_report).resolved + AND NOT (post_report).violates_instance_rules), 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; +AND a.id = diff.post_id; RETURN NULL; @@ -406,15 +349,16 @@ $$); CALL r.create_triggers ('comment_report', $$ BEGIN UPDATE - comment_aggregates AS a + comment 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 + (comment_report).comment_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (comment_report).resolved + AND NOT (comment_report).violates_instance_rules), 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; +AND a.id = diff.comment_id; RETURN NULL; @@ -425,7 +369,7 @@ $$); CALL r.create_triggers ('community_report', $$ BEGIN UPDATE - community_aggregates AS a + community AS a SET report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count FROM ( @@ -433,7 +377,7 @@ BEGIN (community_report).community_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (community_report).resolved), 0) AS unresolved_report_count FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (community_report).community_id) AS diff WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0) - AND a.community_id = diff.community_id; + AND a.id = diff.community_id; RETURN NULL; @@ -441,160 +385,7 @@ END; $$); --- These triggers create and update rows in each aggregates table to match its associated table's rows. --- Deleting rows and updating IDs are already handled by `CASCADE` in foreign key constraints. -CREATE FUNCTION r.comment_aggregates_from_comment () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - INSERT INTO comment_aggregates (comment_id, published) - SELECT - id, - published - FROM - new_comment; - RETURN NULL; -END; -$$; - -CREATE TRIGGER aggregates - AFTER INSERT ON comment REFERENCING NEW TABLE AS new_comment - FOR EACH STATEMENT - EXECUTE FUNCTION r.comment_aggregates_from_comment (); - -CREATE FUNCTION r.community_aggregates_from_community () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - INSERT INTO community_aggregates (community_id, published) - SELECT - id, - published - FROM - new_community; - RETURN NULL; -END; -$$; - -CREATE TRIGGER aggregates - AFTER INSERT ON community REFERENCING NEW TABLE AS new_community - FOR EACH STATEMENT - EXECUTE FUNCTION r.community_aggregates_from_community (); - -CREATE FUNCTION r.person_aggregates_from_person () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - INSERT INTO person_aggregates (person_id) - SELECT - id - FROM - new_person; - RETURN NULL; -END; -$$; - -CREATE TRIGGER aggregates - AFTER INSERT ON person REFERENCING NEW TABLE AS new_person - FOR EACH STATEMENT - EXECUTE FUNCTION r.person_aggregates_from_person (); - -CREATE FUNCTION r.post_aggregates_from_post () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - INSERT INTO post_aggregates (post_id, published, newest_comment_time, newest_comment_time_necro, community_id, creator_id, instance_id, featured_community, featured_local) - SELECT - new_post.id, - new_post.published, - new_post.published, - new_post.published, - new_post.community_id, - new_post.creator_id, - community.instance_id, - new_post.featured_community, - new_post.featured_local - FROM - new_post - INNER JOIN community ON community.id = new_post.community_id; - RETURN NULL; -END; -$$; - -CREATE TRIGGER aggregates - AFTER INSERT ON post REFERENCING NEW TABLE AS new_post - FOR EACH STATEMENT - EXECUTE FUNCTION r.post_aggregates_from_post (); - -CREATE FUNCTION r.post_aggregates_from_post_update () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - UPDATE - post_aggregates - SET - featured_community = new_post.featured_community, - featured_local = new_post.featured_local - FROM - new_post - INNER JOIN old_post ON old_post.id = new_post.id - AND (old_post.featured_community, - old_post.featured_local) != (new_post.featured_community, - new_post.featured_local) - WHERE - post_aggregates.post_id = new_post.id; - RETURN NULL; -END; -$$; - -CREATE TRIGGER aggregates_update - AFTER UPDATE ON post REFERENCING OLD TABLE AS old_post NEW TABLE AS new_post - FOR EACH STATEMENT - EXECUTE FUNCTION r.post_aggregates_from_post_update (); - -CREATE FUNCTION r.site_aggregates_from_site () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - -- only 1 row can be in site_aggregates because of the index idx_site_aggregates_1_row_only. - -- we only ever want to have a single value in site_aggregate because the site_aggregate triggers update all rows in that table. - -- a cleaner check would be to insert it for the local_site but that would break assumptions at least in the tests - INSERT INTO site_aggregates (site_id) - VALUES (NEW.id) - ON CONFLICT ((TRUE)) - DO NOTHING; - RETURN NULL; -END; -$$; - -CREATE TRIGGER aggregates - AFTER INSERT ON site - FOR EACH ROW - EXECUTE FUNCTION r.site_aggregates_from_site (); - -- Change the order of some cascading deletions to make deletion triggers run before the deletion of rows that the triggers need to read -CREATE FUNCTION r.delete_comments_before_post () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - DELETE FROM comment AS c - WHERE c.post_id = OLD.id; - RETURN OLD; -END; -$$; - -CREATE TRIGGER delete_comments - BEFORE DELETE ON post - FOR EACH ROW - EXECUTE FUNCTION r.delete_comments_before_post (); - CREATE FUNCTION r.delete_follow_before_person () RETURNS TRIGGER LANGUAGE plpgsql @@ -645,6 +436,16 @@ BEGIN IF NEW.local THEN NEW.ap_id = coalesce(NEW.ap_id, r.local_url ('/post/' || NEW.id::text)); END IF; + -- Set aggregates + NEW.newest_comment_time = NEW.published; + NEW.newest_comment_time_necro = NEW.published; + NEW.instance_id = ( + SELECT + community.instance_id + FROM + community + WHERE + community.id = NEW.community_id); RETURN NEW; END $$; @@ -889,3 +690,164 @@ CALL r.create_inbox_combined_trigger ('person_post_mention'); CALL r.create_inbox_combined_trigger ('private_message'); +-- Prevent using delete instead of uplete on action tables +CREATE FUNCTION r.require_uplete () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF pg_trigger_depth() = 1 AND NOT starts_with (current_query(), '/**/') THEN + RAISE 'using delete instead of uplete is not allowed for this table'; + END IF; + RETURN NULL; +END +$$; + +CREATE TRIGGER require_uplete + BEFORE DELETE ON comment_actions + FOR EACH STATEMENT + EXECUTE FUNCTION r.require_uplete (); + +CREATE TRIGGER require_uplete + BEFORE DELETE ON community_actions + FOR EACH STATEMENT + EXECUTE FUNCTION r.require_uplete (); + +CREATE TRIGGER require_uplete + BEFORE DELETE ON instance_actions + FOR EACH STATEMENT + EXECUTE FUNCTION r.require_uplete (); + +CREATE TRIGGER require_uplete + BEFORE DELETE ON person_actions + FOR EACH STATEMENT + EXECUTE FUNCTION r.require_uplete (); + +CREATE TRIGGER require_uplete + BEFORE DELETE ON post_actions + FOR EACH STATEMENT + EXECUTE FUNCTION r.require_uplete (); + +-- search: (post, comment, community, person) +CREATE PROCEDURE r.create_search_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.search_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + -- TODO need to figure out how to do the other columns here + INSERT INTO search_combined (published, thing_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER search_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.search_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_search_combined_trigger ('post'); + +CALL r.create_search_combined_trigger ('comment'); + +CALL r.create_search_combined_trigger ('community'); + +CALL r.create_search_combined_trigger ('person'); + +-- You also need to triggers to update the `score` column. +-- post | post::score +-- comment | comment_aggregates::score +-- community | community_aggregates::users_active_monthly +-- person | person_aggregates::post_score +-- +-- Post score +CREATE FUNCTION r.search_combined_post_score_update () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + search_combined + SET + score = NEW.score + WHERE + post_id = NEW.id; + RETURN NULL; +END +$$; + +CREATE TRIGGER search_combined_post_score + AFTER UPDATE OF score ON post + FOR EACH ROW + EXECUTE FUNCTION r.search_combined_post_score_update (); + +-- Comment score +CREATE FUNCTION r.search_combined_comment_score_update () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + search_combined + SET + score = NEW.score + WHERE + comment_id = NEW.id; + RETURN NULL; +END +$$; + +CREATE TRIGGER search_combined_comment_score + AFTER UPDATE OF score ON comment + FOR EACH ROW + EXECUTE FUNCTION r.search_combined_comment_score_update (); + +-- Person score +CREATE FUNCTION r.search_combined_person_score_update () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + search_combined + SET + score = NEW.post_score + WHERE + person_id = NEW.id; + RETURN NULL; +END +$$; + +CREATE TRIGGER search_combined_person_score + AFTER UPDATE OF post_score ON person + FOR EACH ROW + EXECUTE FUNCTION r.search_combined_person_score_update (); + +-- Community score +CREATE FUNCTION r.search_combined_community_score_update () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + search_combined + SET + score = NEW.users_active_month + WHERE + community_id = NEW.id; + RETURN NULL; +END +$$; + +CREATE TRIGGER search_combined_community_score + AFTER UPDATE OF users_active_month ON community + FOR EACH ROW + EXECUTE FUNCTION r.search_combined_community_score_update (); + diff --git a/crates/db_schema/replaceable_schema/utils.sql b/crates/db_schema/replaceable_schema/utils.sql index a9b32f3dd..a951f1a20 100644 --- a/crates/db_schema/replaceable_schema/utils.sql +++ b/crates/db_schema/replaceable_schema/utils.sql @@ -33,16 +33,16 @@ now() - published) < '7 days' THEN 0.0 END; -CREATE FUNCTION r.scaled_rank (score numeric, published timestamp with time zone, users_active_month numeric) +CREATE FUNCTION r.scaled_rank (score numeric, published timestamp with time zone, interactions_month numeric) RETURNS double precision LANGUAGE sql IMMUTABLE PARALLEL SAFE -- Add 2 to avoid divide by zero errors -- Default for score = 1, active users = 1, and now, is (0.1728 / log(2 + 1)) = 0.3621 - -- There may need to be a scale factor multiplied to users_active_month, to make + -- There may need to be a scale factor multiplied to interactions_month, to make -- the log curve less pronounced. This can be tuned in the future. RETURN ( - r.hot_rank (score, published) / log(2 + users_active_month) + r.hot_rank (score, published) / log(2 + interactions_month) ); -- For tables with `deleted` and `removed` columns, this function determines which rows to include in a count. @@ -71,7 +71,7 @@ current_setting('lemmy.protocol_and_hostname') || url_path -- not allowed for a `DELETE` trigger) -- * Transition tables are only provided to the trigger function, not to functions that it calls. -- --- This function can only be called once per table. The trigger function body given as the 2nd argument +-- This function can only be called once per table. The trigger function body is given as the 2nd argument -- and can contain these names, which are replaced with a `SELECT` statement in parenthesis if needed: -- * `select_old_rows` -- * `select_new_rows` @@ -212,6 +212,27 @@ GROUP BY END; $$; +-- Community aggregate function for adding up total number of interactions +CREATE OR REPLACE FUNCTION r.community_aggregates_interactions (i text) + RETURNS TABLE ( + count_ bigint, + community_id_ integer) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN query + SELECT + COALESCE(sum(comments + upvotes + downvotes)::bigint, 0) AS count_, + community_id AS community_id_ + FROM + post + WHERE + published >= (CURRENT_TIMESTAMP - i::interval) + 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 diff --git a/crates/db_schema/src/aggregates/comment_aggregates.rs b/crates/db_schema/src/aggregates/comment_aggregates.rs deleted file mode 100644 index b26d27736..000000000 --- a/crates/db_schema/src/aggregates/comment_aggregates.rs +++ /dev/null @@ -1,153 +0,0 @@ -use crate::{ - aggregates::structs::CommentAggregates, - newtypes::CommentId, - schema::comment_aggregates, - utils::{functions::hot_rank, get_conn, DbPool}, -}; -use diesel::{result::Error, ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; - -impl CommentAggregates { - pub async fn read(pool: &mut DbPool<'_>, comment_id: CommentId) -> Result { - let conn = &mut get_conn(pool).await?; - comment_aggregates::table.find(comment_id).first(conn).await - } - - pub async fn update_hot_rank( - pool: &mut DbPool<'_>, - comment_id: CommentId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - - diesel::update(comment_aggregates::table.find(comment_id)) - .set(comment_aggregates::hot_rank.eq(hot_rank( - comment_aggregates::score, - comment_aggregates::published, - ))) - .get_result::(conn) - .await - } -} - -#[cfg(test)] -mod tests { - - use crate::{ - aggregates::comment_aggregates::CommentAggregates, - source::{ - comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, - community::{Community, CommunityInsertForm}, - instance::Instance, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - }, - traits::{Crud, Likeable}, - utils::build_db_pool_for_tests, - }; - use diesel::result::Error; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_comment_agg"); - - let inserted_person = Person::create(pool, &new_person).await?; - - let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_comment_agg"); - - let another_inserted_person = Person::create(pool, &another_person).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "TIL_comment_agg".into(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - let new_post = PostInsertForm::new( - "A test post".into(), - inserted_person.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let child_comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - - let comment_like = CommentLikeForm { - comment_id: inserted_comment.id, - person_id: inserted_person.id, - score: 1, - }; - - CommentLike::like(pool, &comment_like).await?; - - let comment_aggs_before_delete = CommentAggregates::read(pool, inserted_comment.id).await?; - - assert_eq!(1, comment_aggs_before_delete.score); - assert_eq!(1, comment_aggs_before_delete.upvotes); - assert_eq!(0, comment_aggs_before_delete.downvotes); - - // Add a post dislike from the other person - let comment_dislike = CommentLikeForm { - comment_id: inserted_comment.id, - person_id: another_inserted_person.id, - score: -1, - }; - - CommentLike::like(pool, &comment_dislike).await?; - - let comment_aggs_after_dislike = CommentAggregates::read(pool, inserted_comment.id).await?; - - assert_eq!(0, comment_aggs_after_dislike.score); - assert_eq!(1, comment_aggs_after_dislike.upvotes); - assert_eq!(1, comment_aggs_after_dislike.downvotes); - - // Remove the first comment like - CommentLike::remove(pool, inserted_person.id, inserted_comment.id).await?; - let after_like_remove = CommentAggregates::read(pool, inserted_comment.id).await?; - assert_eq!(-1, after_like_remove.score); - assert_eq!(0, after_like_remove.upvotes); - assert_eq!(1, after_like_remove.downvotes); - - // Remove the parent post - Post::delete(pool, inserted_post.id).await?; - - // Should be none found, since the post was deleted - let after_delete = CommentAggregates::read(pool, inserted_comment.id).await; - assert!(after_delete.is_err()); - - // This should delete all the associated rows, and fire triggers - Person::delete(pool, another_inserted_person.id).await?; - let person_num_deleted = Person::delete(pool, inserted_person.id).await?; - assert_eq!(1, person_num_deleted); - - // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id).await?; - assert_eq!(1, community_num_deleted); - - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } -} diff --git a/crates/db_schema/src/aggregates/community_aggregates.rs b/crates/db_schema/src/aggregates/community_aggregates.rs deleted file mode 100644 index 3ec56d73d..000000000 --- a/crates/db_schema/src/aggregates/community_aggregates.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::{ - aggregates::structs::CommunityAggregates, - newtypes::CommunityId, - schema::{community_aggregates, community_aggregates::subscribers}, - utils::{get_conn, DbPool}, -}; -use diesel::{result::Error, ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; - -impl CommunityAggregates { - pub async fn read(pool: &mut DbPool<'_>, for_community_id: CommunityId) -> Result { - let conn = &mut get_conn(pool).await?; - community_aggregates::table - .find(for_community_id) - .first(conn) - .await - } - - pub async fn update_federated_followers( - pool: &mut DbPool<'_>, - for_community_id: CommunityId, - new_subscribers: i32, - ) -> Result { - let conn = &mut get_conn(pool).await?; - let new_subscribers: i64 = new_subscribers.into(); - diesel::update(community_aggregates::table.find(for_community_id)) - .set(subscribers.eq(new_subscribers)) - .get_result(conn) - .await - } -} - -#[cfg(test)] -mod tests { - - use crate::{ - aggregates::community_aggregates::CommunityAggregates, - source::{ - comment::{Comment, CommentInsertForm}, - community::{ - Community, - CommunityFollower, - CommunityFollowerForm, - CommunityFollowerState, - CommunityInsertForm, - }, - instance::Instance, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - }, - traits::{Crud, Followable}, - utils::build_db_pool_for_tests, - }; - use diesel::result::Error; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - - let inserted_person = Person::create(pool, &new_person).await?; - - let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); - - let another_inserted_person = Person::create(pool, &another_person).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "TIL_community_agg".into(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - let another_community = CommunityInsertForm::new( - inserted_instance.id, - "TIL_community_agg_2".into(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let another_inserted_community = Community::create(pool, &another_community).await?; - - let first_person_follow = CommunityFollowerForm { - community_id: inserted_community.id, - person_id: inserted_person.id, - state: Some(CommunityFollowerState::Accepted), - approver_id: None, - }; - - CommunityFollower::follow(pool, &first_person_follow).await?; - - let second_person_follow = CommunityFollowerForm { - community_id: inserted_community.id, - person_id: another_inserted_person.id, - state: Some(CommunityFollowerState::Accepted), - approver_id: None, - }; - - CommunityFollower::follow(pool, &second_person_follow).await?; - - let another_community_follow = CommunityFollowerForm { - community_id: another_inserted_community.id, - person_id: inserted_person.id, - state: Some(CommunityFollowerState::Accepted), - approver_id: None, - }; - - CommunityFollower::follow(pool, &another_community_follow).await?; - - let new_post = PostInsertForm::new( - "A test post".into(), - inserted_person.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let child_comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - - let community_aggregates_before_delete = - CommunityAggregates::read(pool, inserted_community.id).await?; - - assert_eq!(2, community_aggregates_before_delete.subscribers); - assert_eq!(2, community_aggregates_before_delete.subscribers_local); - assert_eq!(1, community_aggregates_before_delete.posts); - assert_eq!(2, community_aggregates_before_delete.comments); - - // Test the other community - let another_community_aggs = - CommunityAggregates::read(pool, another_inserted_community.id).await?; - assert_eq!(1, another_community_aggs.subscribers); - assert_eq!(1, another_community_aggs.subscribers_local); - assert_eq!(0, another_community_aggs.posts); - assert_eq!(0, another_community_aggs.comments); - - // Unfollow test - CommunityFollower::unfollow(pool, &second_person_follow).await?; - let after_unfollow = CommunityAggregates::read(pool, inserted_community.id).await?; - assert_eq!(1, after_unfollow.subscribers); - assert_eq!(1, after_unfollow.subscribers_local); - - // Follow again just for the later tests - CommunityFollower::follow(pool, &second_person_follow).await?; - let after_follow_again = CommunityAggregates::read(pool, inserted_community.id).await?; - assert_eq!(2, after_follow_again.subscribers); - assert_eq!(2, after_follow_again.subscribers_local); - - // Remove a parent post (the comment count should also be 0) - Post::delete(pool, inserted_post.id).await?; - let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id).await?; - assert_eq!(0, after_parent_post_delete.comments); - assert_eq!(0, after_parent_post_delete.posts); - - // Remove the 2nd person - Person::delete(pool, another_inserted_person.id).await?; - let after_person_delete = CommunityAggregates::read(pool, inserted_community.id).await?; - assert_eq!(1, after_person_delete.subscribers); - assert_eq!(1, after_person_delete.subscribers_local); - - // This should delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await?; - assert_eq!(1, person_num_deleted); - - // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id).await?; - assert_eq!(1, community_num_deleted); - - let another_community_num_deleted = - Community::delete(pool, another_inserted_community.id).await?; - assert_eq!(1, another_community_num_deleted); - - // Should be none found, since the creator was deleted - let after_delete = CommunityAggregates::read(pool, inserted_community.id).await; - assert!(after_delete.is_err()); - - Ok(()) - } -} diff --git a/crates/db_schema/src/aggregates/mod.rs b/crates/db_schema/src/aggregates/mod.rs deleted file mode 100644 index d55f188f3..000000000 --- a/crates/db_schema/src/aggregates/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[cfg(feature = "full")] -pub mod comment_aggregates; -#[cfg(feature = "full")] -pub mod community_aggregates; -#[cfg(feature = "full")] -pub mod person_aggregates; -#[cfg(feature = "full")] -pub mod person_post_aggregates; -#[cfg(feature = "full")] -pub mod post_aggregates; -#[cfg(feature = "full")] -pub mod site_aggregates; -pub mod structs; diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs deleted file mode 100644 index df7004d0e..000000000 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::{ - aggregates::structs::PersonAggregates, - newtypes::PersonId, - schema::person_aggregates, - utils::{get_conn, DbPool}, -}; -use diesel::{result::Error, QueryDsl}; -use diesel_async::RunQueryDsl; - -impl PersonAggregates { - pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { - let conn = &mut get_conn(pool).await?; - person_aggregates::table.find(person_id).first(conn).await - } -} - -#[cfg(test)] -mod tests { - - use crate::{ - aggregates::person_aggregates::PersonAggregates, - source::{ - comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, - community::{Community, CommunityInsertForm}, - instance::Instance, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm, PostLike, PostLikeForm}, - }, - traits::{Crud, Likeable}, - utils::build_db_pool_for_tests, - }; - use diesel::result::Error; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_user_agg"); - - let inserted_person = Person::create(pool, &new_person).await?; - - let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_user_agg"); - - let another_inserted_person = Person::create(pool, &another_person).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "TIL_site_agg".into(), - "nada".to_owned(), - "pubkey".to_string(), - ); - - let inserted_community = Community::create(pool, &new_community).await?; - - let new_post = PostInsertForm::new( - "A test post".into(), - inserted_person.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1); - let _inserted_post_like = PostLike::like(pool, &post_like).await?; - - let comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let mut comment_like = CommentLikeForm { - comment_id: inserted_comment.id, - person_id: inserted_person.id, - score: 1, - }; - - let _inserted_comment_like = CommentLike::like(pool, &comment_like).await?; - - let child_comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - - let child_comment_like = CommentLikeForm { - comment_id: inserted_child_comment.id, - person_id: another_inserted_person.id, - score: 1, - }; - - let _inserted_child_comment_like = CommentLike::like(pool, &child_comment_like).await?; - - let person_aggregates_before_delete = PersonAggregates::read(pool, inserted_person.id).await?; - - assert_eq!(1, person_aggregates_before_delete.post_count); - assert_eq!(1, person_aggregates_before_delete.post_score); - assert_eq!(2, person_aggregates_before_delete.comment_count); - assert_eq!(2, person_aggregates_before_delete.comment_score); - - // Remove a post like - PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; - let after_post_like_remove = PersonAggregates::read(pool, inserted_person.id).await?; - assert_eq!(0, after_post_like_remove.post_score); - - Comment::update( - pool, - inserted_comment.id, - &CommentUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - Comment::update( - pool, - inserted_child_comment.id, - &CommentUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - - let after_parent_comment_removed = PersonAggregates::read(pool, inserted_person.id).await?; - assert_eq!(0, after_parent_comment_removed.comment_count); - // TODO: fix person aggregate comment score calculation - // assert_eq!(0, after_parent_comment_removed.comment_score); - - // Remove a parent comment (the scores should also be removed) - Comment::delete(pool, inserted_comment.id).await?; - Comment::delete(pool, inserted_child_comment.id).await?; - let after_parent_comment_delete = PersonAggregates::read(pool, inserted_person.id).await?; - assert_eq!(0, after_parent_comment_delete.comment_count); - // TODO: fix person aggregate comment score calculation - // assert_eq!(0, after_parent_comment_delete.comment_score); - - // Add in the two comments again, then delete the post. - let new_parent_comment = Comment::create(pool, &comment_form, None).await?; - let _new_child_comment = - Comment::create(pool, &child_comment_form, Some(&new_parent_comment.path)).await?; - comment_like.comment_id = new_parent_comment.id; - CommentLike::like(pool, &comment_like).await?; - let after_comment_add = PersonAggregates::read(pool, inserted_person.id).await?; - assert_eq!(2, after_comment_add.comment_count); - // TODO: fix person aggregate comment score calculation - // assert_eq!(1, after_comment_add.comment_score); - - Post::delete(pool, inserted_post.id).await?; - let after_post_delete = PersonAggregates::read(pool, inserted_person.id).await?; - // TODO: fix person aggregate comment score calculation - // assert_eq!(0, after_post_delete.comment_score); - assert_eq!(0, after_post_delete.comment_count); - assert_eq!(0, after_post_delete.post_score); - assert_eq!(0, after_post_delete.post_count); - - // This should delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await?; - assert_eq!(1, person_num_deleted); - Person::delete(pool, another_inserted_person.id).await?; - - // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id).await?; - assert_eq!(1, community_num_deleted); - - // Should be none found - let after_delete = PersonAggregates::read(pool, inserted_person.id).await; - assert!(after_delete.is_err()); - - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } -} diff --git a/crates/db_schema/src/aggregates/post_aggregates.rs b/crates/db_schema/src/aggregates/post_aggregates.rs deleted file mode 100644 index c11ea6e05..000000000 --- a/crates/db_schema/src/aggregates/post_aggregates.rs +++ /dev/null @@ -1,269 +0,0 @@ -use crate::{ - aggregates::structs::PostAggregates, - newtypes::PostId, - schema::{community_aggregates, post, post_aggregates}, - utils::{ - functions::{hot_rank, scaled_rank}, - get_conn, - DbPool, - }, -}; -use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; -use diesel_async::RunQueryDsl; - -impl PostAggregates { - pub async fn read(pool: &mut DbPool<'_>, post_id: PostId) -> Result { - let conn = &mut get_conn(pool).await?; - post_aggregates::table.find(post_id).first(conn).await - } - - pub async fn update_ranks(pool: &mut DbPool<'_>, post_id: PostId) -> Result { - let conn = &mut get_conn(pool).await?; - - // Diesel can't update based on a join, which is necessary for the scaled_rank - // https://github.com/diesel-rs/diesel/issues/1478 - // Just select the users_active_month manually for now, since its a single post anyway - let users_active_month = community_aggregates::table - .select(community_aggregates::users_active_month) - .inner_join(post::table.on(community_aggregates::community_id.eq(post::community_id))) - .filter(post::id.eq(post_id)) - .first::(conn) - .await?; - - diesel::update(post_aggregates::table.find(post_id)) - .set(( - post_aggregates::hot_rank.eq(hot_rank(post_aggregates::score, post_aggregates::published)), - post_aggregates::hot_rank_active.eq(hot_rank( - post_aggregates::score, - post_aggregates::newest_comment_time_necro, - )), - post_aggregates::scaled_rank.eq(scaled_rank( - post_aggregates::score, - post_aggregates::published, - users_active_month, - )), - )) - .get_result::(conn) - .await - } -} - -#[cfg(test)] -mod tests { - - use crate::{ - aggregates::post_aggregates::PostAggregates, - source::{ - comment::{Comment, CommentInsertForm, CommentUpdateForm}, - community::{Community, CommunityInsertForm}, - instance::Instance, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm, PostLike, PostLikeForm}, - }, - traits::{Crud, Likeable}, - utils::build_db_pool_for_tests, - }; - use diesel::result::Error; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - - let inserted_person = Person::create(pool, &new_person).await?; - - let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); - - let another_inserted_person = Person::create(pool, &another_person).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "TIL_community_agg".into(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - let new_post = PostInsertForm::new( - "A test post".into(), - inserted_person.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let child_comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - - let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1); - - PostLike::like(pool, &post_like).await?; - - let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id).await?; - - assert_eq!(2, post_aggs_before_delete.comments); - assert_eq!(1, post_aggs_before_delete.score); - assert_eq!(1, post_aggs_before_delete.upvotes); - assert_eq!(0, post_aggs_before_delete.downvotes); - - // Add a post dislike from the other person - let post_dislike = PostLikeForm::new(inserted_post.id, another_inserted_person.id, -1); - - PostLike::like(pool, &post_dislike).await?; - - let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id).await?; - - assert_eq!(2, post_aggs_after_dislike.comments); - assert_eq!(0, post_aggs_after_dislike.score); - assert_eq!(1, post_aggs_after_dislike.upvotes); - assert_eq!(1, post_aggs_after_dislike.downvotes); - - // Remove the comments - Comment::delete(pool, inserted_comment.id).await?; - Comment::delete(pool, inserted_child_comment.id).await?; - let after_comment_delete = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(0, after_comment_delete.comments); - assert_eq!(0, after_comment_delete.score); - assert_eq!(1, after_comment_delete.upvotes); - assert_eq!(1, after_comment_delete.downvotes); - - // Remove the first post like - PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; - let after_like_remove = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(0, after_like_remove.comments); - assert_eq!(-1, after_like_remove.score); - assert_eq!(0, after_like_remove.upvotes); - assert_eq!(1, after_like_remove.downvotes); - - // This should delete all the associated rows, and fire triggers - Person::delete(pool, another_inserted_person.id).await?; - let person_num_deleted = Person::delete(pool, inserted_person.id).await?; - assert_eq!(1, person_num_deleted); - - // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id).await?; - assert_eq!(1, community_num_deleted); - - // Should be none found, since the creator was deleted - let after_delete = PostAggregates::read(pool, inserted_post.id).await; - assert!(after_delete.is_err()); - - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } - - #[tokio::test] - #[serial] - async fn test_soft_delete() -> Result<(), Error> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - - let inserted_person = Person::create(pool, &new_person).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "TIL_community_agg".into(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - let new_post = PostInsertForm::new( - "A test post".into(), - inserted_person.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let post_aggregates_before = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(1, post_aggregates_before.comments); - - Comment::update( - pool, - inserted_comment.id, - &CommentUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - - let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(0, post_aggregates_after_remove.comments); - - Comment::update( - pool, - inserted_comment.id, - &CommentUpdateForm { - removed: Some(false), - ..Default::default() - }, - ) - .await?; - - Comment::update( - pool, - inserted_comment.id, - &CommentUpdateForm { - deleted: Some(true), - ..Default::default() - }, - ) - .await?; - - let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(0, post_aggregates_after_delete.comments); - - Comment::update( - pool, - inserted_comment.id, - &CommentUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - - let post_aggregates_after_delete_remove = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(0, post_aggregates_after_delete_remove.comments); - - Comment::delete(pool, inserted_comment.id).await?; - Post::delete(pool, inserted_post.id).await?; - Person::delete(pool, inserted_person.id).await?; - Community::delete(pool, inserted_community.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } -} diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs deleted file mode 100644 index 2df566290..000000000 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::{ - aggregates::structs::SiteAggregates, - schema::site_aggregates, - utils::{get_conn, DbPool}, -}; -use diesel::result::Error; -use diesel_async::RunQueryDsl; - -impl SiteAggregates { - pub async fn read(pool: &mut DbPool<'_>) -> Result { - let conn = &mut get_conn(pool).await?; - site_aggregates::table.first(conn).await - } -} - -#[cfg(test)] -mod tests { - - use crate::{ - aggregates::site_aggregates::SiteAggregates, - source::{ - comment::{Comment, CommentInsertForm}, - community::{Community, CommunityInsertForm, CommunityUpdateForm}, - instance::Instance, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - site::{Site, SiteInsertForm}, - }, - traits::Crud, - utils::{build_db_pool_for_tests, DbPool}, - }; - use diesel::result::Error; - use pretty_assertions::assert_eq; - use serial_test::serial; - - async fn prepare_site_with_community( - pool: &mut DbPool<'_>, - ) -> Result<(Instance, Person, Site, Community), Error> { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_site_agg"); - - let inserted_person = Person::create(pool, &new_person).await?; - - let site_form = SiteInsertForm::new("test_site".into(), inserted_instance.id); - let inserted_site = Site::create(pool, &site_form).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "TIL_site_agg".into(), - "nada".to_owned(), - "pubkey".to_string(), - ); - - let inserted_community = Community::create(pool, &new_community).await?; - - Ok(( - inserted_instance, - inserted_person, - inserted_site, - inserted_community, - )) - } - - #[tokio::test] - #[serial] - async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let (inserted_instance, inserted_person, inserted_site, inserted_community) = - prepare_site_with_community(pool).await?; - - let new_post = PostInsertForm::new( - "A test post".into(), - inserted_person.id, - inserted_community.id, - ); - - // Insert two of those posts - let inserted_post = Post::create(pool, &new_post).await?; - let _inserted_post_again = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - - // Insert two of those comments - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let child_comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - - let site_aggregates_before_delete = SiteAggregates::read(pool).await?; - - // TODO: this is unstable, sometimes it returns 0 users, sometimes 1 - //assert_eq!(0, site_aggregates_before_delete.users); - assert_eq!(1, site_aggregates_before_delete.communities); - assert_eq!(2, site_aggregates_before_delete.posts); - assert_eq!(2, site_aggregates_before_delete.comments); - - // Try a post delete - Post::delete(pool, inserted_post.id).await?; - let site_aggregates_after_post_delete = SiteAggregates::read(pool).await?; - assert_eq!(1, site_aggregates_after_post_delete.posts); - assert_eq!(0, site_aggregates_after_post_delete.comments); - - // This shouuld delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await?; - assert_eq!(1, person_num_deleted); - - // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id).await?; - assert_eq!(1, community_num_deleted); - - // Site should still exist, it can without a site creator. - let after_delete_creator = SiteAggregates::read(pool).await; - assert!(after_delete_creator.is_ok()); - - Site::delete(pool, inserted_site.id).await?; - let after_delete_site = SiteAggregates::read(pool).await; - assert!(after_delete_site.is_err()); - - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } - - #[tokio::test] - #[serial] - async fn test_soft_delete() -> Result<(), Error> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let (inserted_instance, inserted_person, inserted_site, inserted_community) = - prepare_site_with_community(pool).await?; - - let site_aggregates_before = SiteAggregates::read(pool).await?; - assert_eq!(1, site_aggregates_before.communities); - - Community::update( - pool, - inserted_community.id, - &CommunityUpdateForm { - deleted: Some(true), - ..Default::default() - }, - ) - .await?; - - let site_aggregates_after_delete = SiteAggregates::read(pool).await?; - assert_eq!(0, site_aggregates_after_delete.communities); - - Community::update( - pool, - inserted_community.id, - &CommunityUpdateForm { - deleted: Some(false), - ..Default::default() - }, - ) - .await?; - - Community::update( - pool, - inserted_community.id, - &CommunityUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - - let site_aggregates_after_remove = SiteAggregates::read(pool).await?; - assert_eq!(0, site_aggregates_after_remove.communities); - - Community::update( - pool, - inserted_community.id, - &CommunityUpdateForm { - deleted: Some(true), - ..Default::default() - }, - ) - .await?; - - let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await?; - assert_eq!(0, site_aggregates_after_remove_delete.communities); - - Community::delete(pool, inserted_community.id).await?; - Site::delete(pool, inserted_site.id).await?; - Person::delete(pool, inserted_person.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } -} diff --git a/crates/db_schema/src/aggregates/structs.rs b/crates/db_schema/src/aggregates/structs.rs deleted file mode 100644 index a254b0a63..000000000 --- a/crates/db_schema/src/aggregates/structs.rs +++ /dev/null @@ -1,216 +0,0 @@ -use crate::newtypes::{CommentId, CommunityId, InstanceId, PersonId, PostId, SiteId}; -#[cfg(feature = "full")] -use crate::schema::{ - comment_aggregates, - community_aggregates, - person_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")] -use ts_rs::TS; -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable, TS) -)] -#[cfg_attr(feature = "full", diesel(table_name = comment_aggregates))] -#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(primary_key(comment_id)))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// Aggregate data for a comment. -pub struct CommentAggregates { - pub comment_id: CommentId, - pub score: i64, - pub upvotes: i64, - pub downvotes: i64, - pub published: DateTime, - /// The total number of children in this comment branch. - pub child_count: i32, - #[serde(skip)] - 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)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable, TS) -)] -#[cfg_attr(feature = "full", diesel(table_name = community_aggregates))] -#[cfg_attr( - feature = "full", - diesel(belongs_to(crate::source::community::Community)) -)] -#[cfg_attr(feature = "full", diesel(primary_key(community_id)))] -#[cfg_attr(feature = "full", ts(export))] -/// Aggregate data for a community. -pub struct CommunityAggregates { - pub community_id: CommunityId, - pub subscribers: i64, - pub posts: i64, - pub comments: i64, - pub published: DateTime, - /// The number of users with any activity in the last day. - pub users_active_day: i64, - /// The number of users with any activity in the last week. - pub users_active_week: i64, - /// The number of users with any activity in the last month. - pub users_active_month: i64, - /// The number of users with any activity in the last year. - pub users_active_half_year: i64, - #[serde(skip)] - pub hot_rank: f64, - pub subscribers_local: i64, - pub report_count: i16, - pub unresolved_report_count: i16, -} - -#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable, TS) -)] -#[cfg_attr(feature = "full", diesel(table_name = person_aggregates))] -#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] -#[cfg_attr(feature = "full", diesel(primary_key(person_id)))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// Aggregate data for a person. -pub struct PersonAggregates { - pub person_id: PersonId, - pub post_count: i64, - #[serde(skip)] - pub post_score: i64, - pub comment_count: i64, - #[serde(skip)] - pub comment_score: i64, -} - -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] -#[cfg_attr( - feature = "full", - derive( - Queryable, - Selectable, - Associations, - Identifiable, - TS, - CursorKeysModule - ) -)] -#[cfg_attr(feature = "full", diesel(table_name = post_aggregates))] -#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id)))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -#[cfg_attr(feature = "full", cursor_keys_module(name = post_aggregates_keys))] -/// Aggregate data for a post. -pub struct PostAggregates { - pub post_id: PostId, - pub comments: i64, - pub score: i64, - pub upvotes: i64, - pub downvotes: i64, - pub published: DateTime, - #[serde(skip)] - /// A newest comment time, limited to 2 days, to prevent necrobumping - pub newest_comment_time_necro: DateTime, - /// The time of the newest comment in the post. - pub newest_comment_time: DateTime, - /// If the post is featured on the community. - #[serde(skip)] - pub featured_community: bool, - /// If the post is featured on the site / to local. - #[serde(skip)] - pub featured_local: bool, - #[serde(skip)] - pub hot_rank: f64, - #[serde(skip)] - pub hot_rank_active: f64, - #[serde(skip)] - pub community_id: CommunityId, - #[serde(skip)] - pub creator_id: PersonId, - #[serde(skip)] - pub controversy_rank: f64, - #[serde(skip)] - pub instance_id: InstanceId, - /// 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)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable) -)] -#[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)))] -/// Aggregate data for a person's post. -pub struct PersonPostAggregates { - pub person_id: PersonId, - pub post_id: PostId, - /// 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 = 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, -} - -#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy, Hash)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable, TS) -)] -#[cfg_attr(feature = "full", diesel(table_name = site_aggregates))] -#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::site::Site)))] -#[cfg_attr(feature = "full", diesel(primary_key(site_id)))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// Aggregate data for a site. -pub struct SiteAggregates { - pub site_id: SiteId, - pub users: i64, - pub posts: i64, - pub comments: i64, - pub communities: i64, - /// The number of users with any activity in the last day. - pub users_active_day: i64, - /// The number of users with any activity in the last week. - pub users_active_week: i64, - /// The number of users with any activity in the last month. - pub users_active_month: i64, - /// The number of users with any activity in the last half year. - pub users_active_half_year: i64, -} diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index ab322f0cd..4c0cfc0fc 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -61,7 +61,7 @@ impl LocalUserLanguage { for_local_user_id: LocalUserId, ) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; - let mut lang_ids = convert_update_languages(conn, language_ids).await?; + let lang_ids = convert_update_languages(conn, language_ids).await?; // No need to update if languages are unchanged let current = LocalUserLanguage::read(&mut conn.into(), for_local_user_id).await?; @@ -69,16 +69,6 @@ impl LocalUserLanguage { return Ok(()); } - // TODO: Force enable undetermined language for all users. This is necessary because many posts - // don't have a language tag (e.g. those from other federated platforms), so Lemmy users - // won't see them if undetermined language is disabled. - // This hack can be removed once a majority of posts have language tags, or when it is - // clearer for new users that they need to enable undetermined language. - // See https://github.com/LemmyNet/lemmy-ui/issues/999 - if !lang_ids.contains(&UNDETERMINED_ID) { - lang_ids.push(UNDETERMINED_ID); - } - conn .build_transaction() .run(|conn| { @@ -531,7 +521,7 @@ mod tests { let test_langs2 = test_langs2(pool).await?; LocalUserLanguage::update(pool, test_langs2, local_user.id).await?; let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await?; - assert_eq!(3, local_user_langs2.len()); + assert_eq!(2, local_user_langs2.len()); Person::delete(pool, person.id).await?; LocalUser::delete(pool, local_user.id).await?; diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 17cd6ce5c..c81fc2cb3 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -1,5 +1,6 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, + impls::local_user::local_user_can_mod, newtypes::{CommentId, DbUrl, PersonId}, schema::{comment, comment_actions}, source::comment::{ @@ -12,19 +13,28 @@ use crate::{ CommentUpdateForm, }, traits::{Crud, Likeable, Saveable}, - utils::{functions::coalesce, get_conn, now, uplete, DbPool, DELETED_REPLACEMENT_TEXT}, + utils::{ + functions::{coalesce, hot_rank}, + get_conn, + now, + uplete, + DbPool, + DELETED_REPLACEMENT_TEXT, + }, }; use chrono::{DateTime, Utc}; use diesel::{ - dsl::insert_into, + dsl::{case_when, insert_into, not}, expression::SelectableHelper, result::Error, + BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; use diesel_ltree::Ltree; +use lemmy_utils::{error::LemmyResult, settings::structs::Settings}; use url::Url; impl Comment { @@ -116,9 +126,58 @@ impl Comment { None } } + pub async fn update_hot_rank( + pool: &mut DbPool<'_>, + comment_id: CommentId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + diesel::update(comment::table.find(comment_id)) + .set(comment::hot_rank.eq(hot_rank(comment::score, comment::published))) + .get_result::(conn) + .await + } + pub fn local_url(&self, settings: &Settings) -> LemmyResult { + let domain = settings.get_protocol_and_hostname(); + Ok(Url::parse(&format!("{domain}/comment/{}", self.id))?.into()) + } +} + +/// Selects the comment columns, but gives an empty string for content when +/// deleted or removed, and you're not a mod/admin. +#[diesel::dsl::auto_type] +pub fn comment_select_remove_deletes() -> _ { + let deleted_or_removed = comment::deleted.or(comment::removed); + + // You can only view the content if it hasn't been removed, or you can mod. + let can_view_content = not(deleted_or_removed).or(local_user_can_mod()); + let content = case_when(can_view_content, comment::content).otherwise(""); + + ( + comment::id, + comment::creator_id, + comment::post_id, + content, + comment::removed, + comment::published, + comment::updated, + comment::deleted, + comment::ap_id, + comment::local, + comment::path, + comment::distinguished, + comment::language_id, + comment::score, + comment::upvotes, + comment::downvotes, + comment::child_count, + comment::hot_rank, + comment::controversy_rank, + comment::report_count, + comment::unresolved_report_count, + ) } -#[async_trait] impl Crud for Comment { type InsertForm = CommentInsertForm; type UpdateForm = CommentUpdateForm; @@ -143,7 +202,6 @@ impl Crud for Comment { } } -#[async_trait] impl Likeable for CommentLike { type Form = CommentLikeForm; type IdType = CommentId; @@ -176,7 +234,6 @@ impl Likeable for CommentLike { } } -#[async_trait] impl Saveable for CommentSaved { type Form = CommentSavedForm; async fn save( @@ -210,25 +267,17 @@ impl Saveable for CommentSaved { #[cfg(test)] mod tests { + use super::*; use crate::{ newtypes::LanguageId, source::{ - comment::{ - Comment, - CommentInsertForm, - CommentLike, - CommentLikeForm, - CommentSaved, - CommentSavedForm, - CommentUpdateForm, - }, community::{Community, CommunityInsertForm}, instance::Instance, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm}, }, traits::{Crud, Likeable, Saveable}, - utils::{build_db_pool_for_tests, uplete}, + utils::{build_db_pool_for_tests, uplete, RANK_DEFAULT}, }; use diesel_ltree::Ltree; use lemmy_utils::error::LemmyResult; @@ -288,6 +337,14 @@ mod tests { distinguished: false, local: true, language_id: LanguageId::default(), + child_count: 1, + controversy_rank: 0.0, + downvotes: 0, + upvotes: 1, + score: 1, + hot_rank: RANK_DEFAULT, + report_count: 0, + unresolved_report_count: 0, }; let child_comment_form = CommentInsertForm::new( @@ -342,7 +399,6 @@ mod tests { Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_comment, read_comment); - assert_eq!(expected_comment, inserted_comment); assert_eq!(expected_comment, updated_comment); assert_eq!(expected_comment_like, inserted_comment_like); assert_eq!(expected_comment_saved, inserted_comment_saved); @@ -356,4 +412,107 @@ mod tests { Ok(()) } + + #[tokio::test] + #[serial] + async fn test_aggregates() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_comment_agg"); + + let inserted_person = Person::create(pool, &new_person).await?; + + let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_comment_agg"); + + let another_inserted_person = Person::create(pool, &another_person).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_comment_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; + + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; + + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let _inserted_child_comment = + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; + + let comment_like = CommentLikeForm { + comment_id: inserted_comment.id, + person_id: inserted_person.id, + score: 1, + }; + + CommentLike::like(pool, &comment_like).await?; + + let comment_aggs_before_delete = Comment::read(pool, inserted_comment.id).await?; + + assert_eq!(1, comment_aggs_before_delete.score); + assert_eq!(1, comment_aggs_before_delete.upvotes); + assert_eq!(0, comment_aggs_before_delete.downvotes); + + // Add a post dislike from the other person + let comment_dislike = CommentLikeForm { + comment_id: inserted_comment.id, + person_id: another_inserted_person.id, + score: -1, + }; + + CommentLike::like(pool, &comment_dislike).await?; + + let comment_aggs_after_dislike = Comment::read(pool, inserted_comment.id).await?; + + assert_eq!(0, comment_aggs_after_dislike.score); + assert_eq!(1, comment_aggs_after_dislike.upvotes); + assert_eq!(1, comment_aggs_after_dislike.downvotes); + + // Remove the first comment like + CommentLike::remove(pool, inserted_person.id, inserted_comment.id).await?; + let after_like_remove = Comment::read(pool, inserted_comment.id).await?; + assert_eq!(-1, after_like_remove.score); + assert_eq!(0, after_like_remove.upvotes); + assert_eq!(1, after_like_remove.downvotes); + + // Remove the parent post + Post::delete(pool, inserted_post.id).await?; + + // Should be none found, since the post was deleted + let after_delete = Comment::read(pool, inserted_comment.id).await; + assert!(after_delete.is_err()); + + // This should delete all the associated rows, and fire triggers + Person::delete(pool, another_inserted_person.id).await?; + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; + assert_eq!(1, person_num_deleted); + + // Delete the community + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; + assert_eq!(1, community_num_deleted); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } } diff --git a/crates/db_schema/src/impls/comment_reply.rs b/crates/db_schema/src/impls/comment_reply.rs index 5a33a51d7..47dfaa9af 100644 --- a/crates/db_schema/src/impls/comment_reply.rs +++ b/crates/db_schema/src/impls/comment_reply.rs @@ -9,7 +9,6 @@ use crate::{ use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for CommentReply { type InsertForm = CommentReplyInsertForm; type UpdateForm = CommentReplyUpdateForm; diff --git a/crates/db_schema/src/impls/comment_report.rs b/crates/db_schema/src/impls/comment_report.rs index 4c6a1e0d0..f8ff7d6c9 100644 --- a/crates/db_schema/src/impls/comment_report.rs +++ b/crates/db_schema/src/impls/comment_report.rs @@ -1,9 +1,6 @@ use crate::{ newtypes::{CommentId, CommentReportId, PersonId}, - schema::comment_report::{ - comment_id, - dsl::{comment_report, resolved, resolver_id, updated}, - }, + schema::comment_report, source::comment_report::{CommentReport, CommentReportForm}, traits::Reportable, utils::{get_conn, DbPool}, @@ -12,12 +9,13 @@ use chrono::Utc; use diesel::{ dsl::{insert_into, update}, result::Error, + BoolExpressionMethods, ExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; -#[async_trait] impl Reportable for CommentReport { type Form = CommentReportForm; type IdType = CommentReportId; @@ -31,7 +29,7 @@ impl Reportable for CommentReport { comment_report_form: &CommentReportForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(comment_report) + insert_into(comment_report::table) .values(comment_report_form) .get_result::(conn) .await @@ -48,27 +46,52 @@ impl Reportable for CommentReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(comment_report.find(report_id_)) + update(comment_report::table.find(report_id_)) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + comment_report::resolved.eq(true), + comment_report::resolver_id.eq(by_resolver_id), + comment_report::updated.eq(Utc::now()), )) .execute(conn) .await } + async fn resolve_apub( + pool: &mut DbPool<'_>, + object_id: Self::ObjectIdType, + report_creator_id: PersonId, + resolver_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + Ok( + update( + comment_report::table.filter( + comment_report::comment_id + .eq(object_id) + .and(comment_report::creator_id.eq(report_creator_id)), + ), + ) + .set(( + comment_report::resolved.eq(true), + comment_report::resolver_id.eq(resolver_id), + comment_report::updated.eq(Utc::now()), + )) + .execute(conn) + .await?, + ) + } + async fn resolve_all_for_object( pool: &mut DbPool<'_>, comment_id_: CommentId, by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(comment_report.filter(comment_id.eq(comment_id_))) + update(comment_report::table.filter(comment_report::comment_id.eq(comment_id_))) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + comment_report::resolved.eq(true), + comment_report::resolver_id.eq(by_resolver_id), + comment_report::updated.eq(Utc::now()), )) .execute(conn) .await @@ -85,11 +108,11 @@ impl Reportable for CommentReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(comment_report.find(report_id_)) + update(comment_report::table.find(report_id_)) .set(( - resolved.eq(false), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + comment_report::resolved.eq(false), + comment_report::resolver_id.eq(by_resolver_id), + comment_report::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 059565d6b..711dd08cb 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -20,8 +20,6 @@ use crate::{ }, traits::{ApubActor, Bannable, Crud, Followable, Joinable}, utils::{ - action_query, - find_action, functions::{coalesce, coalesce_2_nullable, lower, random_smallint}, get_conn, now, @@ -35,7 +33,7 @@ use crate::{ use chrono::{DateTime, Utc}; use diesel::{ deserialize, - dsl::{self, exists, insert_into, not}, + dsl::{exists, insert_into, not}, expression::SelectableHelper, pg::Pg, result::Error, @@ -49,9 +47,12 @@ use diesel::{ Queryable, }; use diesel_async::RunQueryDsl; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::{ + error::{LemmyErrorType, LemmyResult}, + settings::structs::Settings, +}; +use url::Url; -#[async_trait] impl Crud for Community { type InsertForm = CommunityInsertForm; type UpdateForm = CommunityUpdateForm; @@ -84,7 +85,6 @@ impl Crud for Community { } } -#[async_trait] impl Joinable for CommunityModerator { type Form = CommunityModeratorForm; async fn join( @@ -135,7 +135,7 @@ impl Community { timestamp: DateTime, form: &CommunityInsertForm, ) -> Result { - let is_new_community = match &form.actor_id { + let is_new_community = match &form.ap_id { Some(id) => Community::read_from_apub_id(pool, id).await?.is_none(), None => true, }; @@ -144,7 +144,7 @@ impl Community { // Can't do separate insert/update commands because InsertForm/UpdateForm aren't convertible let community_ = insert_into(community::table) .values(form) - .on_conflict(community::actor_id) + .on_conflict(community::ap_id) .filter_target(coalesce(community::updated, community::published).lt(timestamp)) .do_update() .set(form) @@ -268,6 +268,31 @@ impl Community { .get_result::(conn) .await } + + #[diesel::dsl::auto_type(no_type_alias)] + pub fn hide_removed_and_deleted() -> _ { + community::removed + .eq(false) + .and(community::deleted.eq(false)) + } + + pub fn local_url(name: &str, settings: &Settings) -> LemmyResult { + let domain = settings.get_protocol_and_hostname(); + Ok(Url::parse(&format!("{domain}/c/{name}"))?.into()) + } + + pub async fn update_federated_followers( + pool: &mut DbPool<'_>, + for_community_id: CommunityId, + new_subscribers: i32, + ) -> Result { + let conn = &mut get_conn(pool).await?; + let new_subscribers: i64 = new_subscribers.into(); + diesel::update(community::table.find(for_community_id)) + .set(community::dsl::subscribers.eq(new_subscribers)) + .get_result(conn) + .await + } } impl CommunityModerator { @@ -301,7 +326,8 @@ impl CommunityModerator { for_person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - action_query(community_actions::became_moderator) + community_actions::table + .filter(community_actions::became_moderator.is_not_null()) .filter(community_actions::person_id.eq(for_person_id)) .select(community_actions::community_id) .load::(conn) @@ -322,7 +348,8 @@ impl CommunityModerator { persons.push(mod_person_id); persons.dedup(); - let res = action_query(community_actions::became_moderator) + let res = community_actions::table + .filter(community_actions::became_moderator.is_not_null()) .filter(community_actions::community_id.eq(for_community_id)) .filter(community_actions::person_id.eq_any(persons)) .order_by(community_actions::became_moderator) @@ -340,7 +367,6 @@ impl CommunityModerator { } } -#[async_trait] impl Bannable for CommunityPersonBan { type Form = CommunityPersonBanForm; async fn ban( @@ -382,10 +408,6 @@ impl Bannable for CommunityPersonBan { } impl CommunityFollower { - pub fn select_subscribed_type() -> dsl::Nullable { - community_actions::follow_state.nullable() - } - /// Check if a remote instance has any followers on local instance. For this it is enough to check /// if any follow relation is stored. Dont use this for local community. pub async fn check_has_local_followers( @@ -393,14 +415,14 @@ impl CommunityFollower { remote_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(exists( - action_query(community_actions::followed) - .filter(community_actions::community_id.eq(remote_community_id)), - )) - .get_result::(conn) - .await? - .then_some(()) - .ok_or(LemmyErrorType::CommunityHasNoFollowers.into()) + let find_action = community_actions::table + .filter(community_actions::followed.is_not_null()) + .filter(community_actions::community_id.eq(remote_community_id)); + select(exists(find_action)) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::CommunityHasNoFollowers.into()) } pub async fn approve( @@ -410,20 +432,28 @@ impl CommunityFollower { approver_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - diesel::update(find_action( - community_actions::followed, - (follower_id, community_id), - )) - .set(( - community_actions::follow_state.eq(CommunityFollowerState::Accepted), - community_actions::follow_approver_id.eq(approver_id), - )) - .execute(conn) - .await?; + let find_action = community_actions::table + .find((follower_id, community_id)) + .filter(community_actions::followed.is_not_null()); + diesel::update(find_action) + .set(( + community_actions::follow_state.eq(CommunityFollowerState::Accepted), + community_actions::follow_approver_id.eq(approver_id), + )) + .execute(conn) + .await?; Ok(()) } } +// TODO +// I'd really like to have these on the impl, but unfortunately they have to be top level, +// according to https://diesel.rs/guides/composing-applications.html +#[diesel::dsl::auto_type] +pub fn community_follower_select_subscribed_type() -> _ { + community_actions::follow_state.nullable() +} + impl Queryable, Pg> for SubscribedType { @@ -438,7 +468,6 @@ impl Queryable, form: &CommunityFollowerForm) -> Result { @@ -462,14 +491,14 @@ impl Followable for CommunityFollower { person_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).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 + let find_action = community_actions::table + .find((person_id, community_id)) + .filter(community_actions::follow_state.is_not_null()); + diesel::update(find_action) + .set(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) + .returning(Self::as_select()) + .get_result::(conn) + .await } async fn unfollow( pool: &mut DbPool<'_>, @@ -485,7 +514,6 @@ impl Followable for CommunityFollower { } } -#[async_trait] impl ApubActor for Community { async fn read_from_apub_id( pool: &mut DbPool<'_>, @@ -493,7 +521,7 @@ impl ApubActor for Community { ) -> Result, Error> { let conn = &mut get_conn(pool).await?; community::table - .filter(community::actor_id.eq(object_id)) + .filter(community::ap_id.eq(object_id)) .first(conn) .await .optional() @@ -510,9 +538,7 @@ impl ApubActor for Community { .filter(community::local.eq(true)) .filter(lower(community::name).eq(community_name.to_lowercase())); if !include_deleted { - q = q - .filter(community::deleted.eq(false)) - .filter(community::removed.eq(false)); + q = q.filter(Self::hide_removed_and_deleted()) } q.first(conn).await.optional() } @@ -538,6 +564,7 @@ impl ApubActor for Community { mod tests { use crate::{ source::{ + comment::{Comment, CommentInsertForm}, community::{ Community, CommunityFollower, @@ -553,9 +580,10 @@ mod tests { instance::Instance, local_user::LocalUser, person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, }, traits::{Bannable, Crud, Followable, Joinable}, - utils::{build_db_pool_for_tests, uplete}, + utils::{build_db_pool_for_tests, uplete, RANK_DEFAULT}, CommunityVisibility, }; use lemmy_utils::error::LemmyResult; @@ -595,7 +623,7 @@ mod tests { deleted: false, published: inserted_community.published, updated: None, - actor_id: inserted_community.actor_id.clone(), + ap_id: inserted_community.ap_id.clone(), local: true, private_key: None, public_key: "pubkey".to_owned(), @@ -611,6 +639,18 @@ mod tests { instance_id: inserted_instance.id, visibility: CommunityVisibility::Public, random_number: inserted_community.random_number, + subscribers: 1, + posts: 0, + comments: 0, + users_active_day: 0, + users_active_week: 0, + users_active_month: 0, + users_active_half_year: 0, + hot_rank: RANK_DEFAULT, + subscribers_local: 1, + report_count: 0, + unresolved_report_count: 0, + interactions_month: 0, }; let community_follower_form = CommunityFollowerForm { @@ -718,7 +758,6 @@ mod tests { Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_community, read_community); - assert_eq!(expected_community, inserted_community); assert_eq!(expected_community, updated_community); assert_eq!(expected_community_follower, inserted_community_follower); assert_eq!(expected_community_moderator, inserted_bobby_moderator); @@ -731,4 +770,142 @@ mod tests { Ok(()) } + + #[tokio::test] + #[serial] + async fn test_aggregates() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); + + let inserted_person = Person::create(pool, &new_person).await?; + + let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); + + let another_inserted_person = Person::create(pool, &another_person).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_community_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; + + let another_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_community_agg_2".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let another_inserted_community = Community::create(pool, &another_community).await?; + + let first_person_follow = CommunityFollowerForm { + community_id: inserted_community.id, + person_id: inserted_person.id, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, + }; + + CommunityFollower::follow(pool, &first_person_follow).await?; + + let second_person_follow = CommunityFollowerForm { + community_id: inserted_community.id, + person_id: another_inserted_person.id, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, + }; + + CommunityFollower::follow(pool, &second_person_follow).await?; + + let another_community_follow = CommunityFollowerForm { + community_id: another_inserted_community.id, + person_id: inserted_person.id, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, + }; + + CommunityFollower::follow(pool, &another_community_follow).await?; + + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; + + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let _inserted_child_comment = + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; + + let community_aggregates_before_delete = Community::read(pool, inserted_community.id).await?; + + assert_eq!(2, community_aggregates_before_delete.subscribers); + assert_eq!(2, community_aggregates_before_delete.subscribers_local); + assert_eq!(1, community_aggregates_before_delete.posts); + assert_eq!(2, community_aggregates_before_delete.comments); + + // Test the other community + let another_community_aggs = Community::read(pool, another_inserted_community.id).await?; + assert_eq!(1, another_community_aggs.subscribers); + assert_eq!(1, another_community_aggs.subscribers_local); + assert_eq!(0, another_community_aggs.posts); + assert_eq!(0, another_community_aggs.comments); + + // Unfollow test + CommunityFollower::unfollow(pool, &second_person_follow).await?; + let after_unfollow = Community::read(pool, inserted_community.id).await?; + assert_eq!(1, after_unfollow.subscribers); + assert_eq!(1, after_unfollow.subscribers_local); + + // Follow again just for the later tests + CommunityFollower::follow(pool, &second_person_follow).await?; + let after_follow_again = Community::read(pool, inserted_community.id).await?; + assert_eq!(2, after_follow_again.subscribers); + assert_eq!(2, after_follow_again.subscribers_local); + + // Remove a parent post (the comment count should also be 0) + Post::delete(pool, inserted_post.id).await?; + let after_parent_post_delete = Community::read(pool, inserted_community.id).await?; + assert_eq!(0, after_parent_post_delete.posts); + assert_eq!(0, after_parent_post_delete.comments); + + // Remove the 2nd person + Person::delete(pool, another_inserted_person.id).await?; + let after_person_delete = Community::read(pool, inserted_community.id).await?; + assert_eq!(1, after_person_delete.subscribers); + assert_eq!(1, after_person_delete.subscribers_local); + + // This should delete all the associated rows, and fire triggers + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; + assert_eq!(1, person_num_deleted); + + // Delete the community + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; + assert_eq!(1, community_num_deleted); + + let another_community_num_deleted = + Community::delete(pool, another_inserted_community.id).await?; + assert_eq!(1, another_community_num_deleted); + + // Should be none found, since the creator was deleted + let after_delete = Community::read(pool, inserted_community.id).await; + assert!(after_delete.is_err()); + + Ok(()) + } } diff --git a/crates/db_schema/src/impls/community_block.rs b/crates/db_schema/src/impls/community_block.rs index c520e43e8..67c84c9d2 100644 --- a/crates/db_schema/src/impls/community_block.rs +++ b/crates/db_schema/src/impls/community_block.rs @@ -6,7 +6,7 @@ use crate::{ community_block::{CommunityBlock, CommunityBlockForm}, }, traits::Blockable, - utils::{action_query, find_action, get_conn, now, uplete, DbPool}, + utils::{get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, @@ -27,14 +27,14 @@ impl CommunityBlock { for_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists(find_action( - community_actions::blocked, - (for_person_id, for_community_id), - )))) - .get_result::(conn) - .await? - .then_some(()) - .ok_or(LemmyErrorType::CommunityIsBlocked.into()) + let find_action = community_actions::table + .find((for_person_id, for_community_id)) + .filter(community_actions::blocked.is_not_null()); + select(not(exists(find_action))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::CommunityIsBlocked.into()) } pub async fn for_person( @@ -42,7 +42,8 @@ impl CommunityBlock { person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - action_query(community_actions::blocked) + community_actions::table + .filter(community_actions::blocked.is_not_null()) .inner_join(community::table) .select(community::all_columns) .filter(community_actions::person_id.eq(person_id)) @@ -54,7 +55,6 @@ impl CommunityBlock { } } -#[async_trait] impl Blockable for CommunityBlock { type Form = CommunityBlockForm; async fn block(pool: &mut DbPool<'_>, community_block_form: &Self::Form) -> Result { diff --git a/crates/db_schema/src/impls/community_report.rs b/crates/db_schema/src/impls/community_report.rs index 85c3cc5c0..780a30cf0 100644 --- a/crates/db_schema/src/impls/community_report.rs +++ b/crates/db_schema/src/impls/community_report.rs @@ -1,9 +1,6 @@ use crate::{ newtypes::{CommunityId, CommunityReportId, PersonId}, - schema::community_report::{ - community_id, - dsl::{community_report, resolved, resolver_id, updated}, - }, + schema::community_report, source::community_report::{CommunityReport, CommunityReportForm}, traits::Reportable, utils::{get_conn, DbPool}, @@ -12,12 +9,13 @@ use chrono::Utc; use diesel::{ dsl::{insert_into, update}, result::Error, + BoolExpressionMethods, ExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; -#[async_trait] impl Reportable for CommunityReport { type Form = CommunityReportForm; type IdType = CommunityReportId; @@ -31,7 +29,7 @@ impl Reportable for CommunityReport { community_report_form: &CommunityReportForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_report) + insert_into(community_report::table) .values(community_report_form) .get_result::(conn) .await @@ -48,27 +46,52 @@ impl Reportable for CommunityReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(community_report.find(report_id_)) + update(community_report::table.find(report_id_)) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + community_report::resolved.eq(true), + community_report::resolver_id.eq(by_resolver_id), + community_report::updated.eq(Utc::now()), )) .execute(conn) .await } + async fn resolve_apub( + pool: &mut DbPool<'_>, + object_id: Self::ObjectIdType, + report_creator_id: PersonId, + resolver_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + Ok( + update( + community_report::table.filter( + community_report::community_id + .eq(object_id) + .and(community_report::creator_id.eq(report_creator_id)), + ), + ) + .set(( + community_report::resolved.eq(true), + community_report::resolver_id.eq(resolver_id), + community_report::updated.eq(Utc::now()), + )) + .execute(conn) + .await?, + ) + } + async fn resolve_all_for_object( pool: &mut DbPool<'_>, community_id_: CommunityId, by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(community_report.filter(community_id.eq(community_id_))) + update(community_report::table.filter(community_report::community_id.eq(community_id_))) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + community_report::resolved.eq(true), + community_report::resolver_id.eq(by_resolver_id), + community_report::updated.eq(Utc::now()), )) .execute(conn) .await @@ -85,11 +108,11 @@ impl Reportable for CommunityReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(community_report.find(report_id_)) + update(community_report::table.find(report_id_)) .set(( - resolved.eq(false), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + community_report::resolved.eq(false), + community_report::resolver_id.eq(by_resolver_id), + community_report::updated.eq(Utc::now()), )) .execute(conn) .await diff --git a/crates/db_schema/src/impls/custom_emoji.rs b/crates/db_schema/src/impls/custom_emoji.rs index 9ba359071..703177f35 100644 --- a/crates/db_schema/src/impls/custom_emoji.rs +++ b/crates/db_schema/src/impls/custom_emoji.rs @@ -14,7 +14,6 @@ use crate::{ use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for CustomEmoji { type InsertForm = CustomEmojiInsertForm; type UpdateForm = CustomEmojiUpdateForm; diff --git a/crates/db_schema/src/impls/images.rs b/crates/db_schema/src/impls/images.rs index df894f68d..deb9a9e78 100644 --- a/crates/db_schema/src/impls/images.rs +++ b/crates/db_schema/src/impls/images.rs @@ -1,7 +1,7 @@ use crate::{ newtypes::{DbUrl, LocalUserId}, schema::{image_details, local_image, remote_image}, - source::images::{ImageDetails, ImageDetailsForm, LocalImage, LocalImageForm, RemoteImage}, + source::images::{ImageDetails, ImageDetailsInsertForm, LocalImage, LocalImageForm, RemoteImage}, utils::{get_conn, DbPool}, }; use diesel::{ @@ -21,7 +21,7 @@ impl LocalImage { pub async fn create( pool: &mut DbPool<'_>, form: &LocalImageForm, - image_details_form: &ImageDetailsForm, + image_details_form: &ImageDetailsInsertForm, ) -> Result { let conn = &mut get_conn(pool).await?; conn @@ -66,7 +66,7 @@ impl LocalImage { } pub async fn delete_by_url(pool: &mut DbPool<'_>, url: &DbUrl) -> Result { - let alias = url.as_str().split('/').last().ok_or(NotFound)?; + let alias = url.as_str().split('/').next_back().ok_or(NotFound)?; Self::delete_by_alias(pool, alias).await } } @@ -102,7 +102,10 @@ impl RemoteImage { } impl ImageDetails { - pub async fn create(pool: &mut DbPool<'_>, form: &ImageDetailsForm) -> Result { + pub async fn create( + pool: &mut DbPool<'_>, + form: &ImageDetailsInsertForm, + ) -> Result { let conn = &mut get_conn(pool).await?; insert_into(image_details::table) diff --git a/crates/db_schema/src/impls/instance_block.rs b/crates/db_schema/src/impls/instance_block.rs index 1722e8318..62ad7d297 100644 --- a/crates/db_schema/src/impls/instance_block.rs +++ b/crates/db_schema/src/impls/instance_block.rs @@ -6,7 +6,7 @@ use crate::{ instance_block::{InstanceBlock, InstanceBlockForm}, }, traits::Blockable, - utils::{action_query, find_action, get_conn, now, uplete, DbPool}, + utils::{get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, @@ -27,14 +27,14 @@ impl InstanceBlock { for_instance_id: InstanceId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists(find_action( - instance_actions::blocked, - (for_person_id, for_instance_id), - )))) - .get_result::(conn) - .await? - .then_some(()) - .ok_or(LemmyErrorType::InstanceIsBlocked.into()) + let find_action = instance_actions::table + .find((for_person_id, for_instance_id)) + .filter(instance_actions::blocked.is_not_null()); + select(not(exists(find_action))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::InstanceIsBlocked.into()) } pub async fn for_person( @@ -42,7 +42,8 @@ impl InstanceBlock { person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - action_query(instance_actions::blocked) + instance_actions::table + .filter(instance_actions::blocked.is_not_null()) .inner_join(instance::table) .select(instance::all_columns) .filter(instance_actions::person_id.eq(person_id)) @@ -52,7 +53,6 @@ impl InstanceBlock { } } -#[async_trait] impl Blockable for InstanceBlock { type Form = InstanceBlockForm; async fn block(pool: &mut DbPool<'_>, instance_block_form: &Self::Form) -> Result { diff --git a/crates/db_schema/src/impls/local_site.rs b/crates/db_schema/src/impls/local_site.rs index bdbe4ac6c..85ba82bf9 100644 --- a/crates/db_schema/src/impls/local_site.rs +++ b/crates/db_schema/src/impls/local_site.rs @@ -39,3 +39,195 @@ impl LocalSite { diesel::delete(local_site::table).execute(conn).await } } + +#[cfg(test)] +mod tests { + + use super::*; + use crate::{ + source::{ + comment::{Comment, CommentInsertForm}, + community::{Community, CommunityInsertForm, CommunityUpdateForm}, + instance::Instance, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, + site::{Site, SiteInsertForm}, + }, + traits::Crud, + utils::{build_db_pool_for_tests, DbPool}, + }; + use pretty_assertions::assert_eq; + use serial_test::serial; + + async fn prepare_site_with_community( + pool: &mut DbPool<'_>, + ) -> LemmyResult<(Instance, Person, Site, Community)> { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_site_agg"); + + let inserted_person = Person::create(pool, &new_person).await?; + + let site_form = SiteInsertForm::new("test_site".into(), inserted_instance.id); + let inserted_site = Site::create(pool, &site_form).await?; + + let local_site_form = LocalSiteInsertForm::new(inserted_site.id); + LocalSite::create(pool, &local_site_form).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_site_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + + let inserted_community = Community::create(pool, &new_community).await?; + + Ok(( + inserted_instance, + inserted_person, + inserted_site, + inserted_community, + )) + } + + #[tokio::test] + #[serial] + async fn test_aggregates() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let (inserted_instance, inserted_person, inserted_site, inserted_community) = + prepare_site_with_community(pool).await?; + + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + + // Insert two of those posts + let inserted_post = Post::create(pool, &new_post).await?; + let _inserted_post_again = Post::create(pool, &new_post).await?; + + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + + // Insert two of those comments + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let _inserted_child_comment = + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; + + let site_aggregates_before_delete = LocalSite::read(pool).await?; + + // TODO: this is unstable, sometimes it returns 0 users, sometimes 1 + //assert_eq!(0, site_aggregates_before_delete.users); + assert_eq!(1, site_aggregates_before_delete.communities); + assert_eq!(2, site_aggregates_before_delete.posts); + assert_eq!(2, site_aggregates_before_delete.comments); + + // Try a post delete + Post::delete(pool, inserted_post.id).await?; + let site_aggregates_after_post_delete = LocalSite::read(pool).await?; + assert_eq!(1, site_aggregates_after_post_delete.posts); + assert_eq!(0, site_aggregates_after_post_delete.comments); + + // This shouuld delete all the associated rows, and fire triggers + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; + assert_eq!(1, person_num_deleted); + + // Delete the community + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; + assert_eq!(1, community_num_deleted); + + // Site should still exist, it can without a site creator. + let after_delete_creator = LocalSite::read(pool).await; + assert!(after_delete_creator.is_ok()); + + Site::delete(pool, inserted_site.id).await?; + let after_delete_site = LocalSite::read(pool).await; + assert!(after_delete_site.is_err()); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_soft_delete() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let (inserted_instance, inserted_person, inserted_site, inserted_community) = + prepare_site_with_community(pool).await?; + + let site_aggregates_before = LocalSite::read(pool).await?; + assert_eq!(1, site_aggregates_before.communities); + + Community::update( + pool, + inserted_community.id, + &CommunityUpdateForm { + deleted: Some(true), + ..Default::default() + }, + ) + .await?; + + let site_aggregates_after_delete = LocalSite::read(pool).await?; + assert_eq!(0, site_aggregates_after_delete.communities); + + Community::update( + pool, + inserted_community.id, + &CommunityUpdateForm { + deleted: Some(false), + ..Default::default() + }, + ) + .await?; + + Community::update( + pool, + inserted_community.id, + &CommunityUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + + let site_aggregates_after_remove = LocalSite::read(pool).await?; + assert_eq!(0, site_aggregates_after_remove.communities); + + Community::update( + pool, + inserted_community.id, + &CommunityUpdateForm { + deleted: Some(true), + ..Default::default() + }, + ) + .await?; + + let site_aggregates_after_remove_delete = LocalSite::read(pool).await?; + assert_eq!(0, site_aggregates_after_remove_delete.communities); + + Community::delete(pool, inserted_community.id).await?; + Site::delete(pool, inserted_site.id).await?; + Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } +} diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 5c184113e..e643427c1 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -1,14 +1,13 @@ use crate::{ + aliases::creator_community_actions, newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId}, schema::{community, community_actions, local_user, person, registration_application}, source::{ actor_language::LocalUserLanguage, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, - local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeInsertForm}, site::Site, }, utils::{ - action_query, functions::{coalesce, lower}, get_conn, now, @@ -20,13 +19,19 @@ use bcrypt::{hash, DEFAULT_COST}; use diesel::{ dsl::{insert_into, not, IntervalDsl}, result::Error, + BoolExpressionMethods, CombineDsl, ExpressionMethods, JoinOnDsl, + NullableExpressionMethods, + PgExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::{ + email::{lang_str_to_lang, translations::Lang}, + error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, +}; impl LocalUser { pub async fn create( @@ -49,10 +54,6 @@ impl LocalUser { LocalUserLanguage::update(pool, languages, local_user_.id).await?; - // Create their vote_display_modes - let vote_display_mode_form = LocalUserVoteDisplayModeInsertForm::new(local_user_.id); - LocalUserVoteDisplayMode::create(pool, &vote_display_mode_form).await?; - Ok(local_user_) } @@ -168,42 +169,48 @@ impl LocalUser { }; let conn = &mut get_conn(pool).await?; - let followed_communities = action_query(community_actions::followed) + let followed_communities = community_actions::table + .filter(community_actions::followed.is_not_null()) .filter(community_actions::person_id.eq(person_id_)) .inner_join(community::table) - .select(community::actor_id) + .select(community::ap_id) .get_results(conn) .await?; - let saved_posts = action_query(post_actions::saved) + let saved_posts = post_actions::table + .filter(post_actions::saved.is_not_null()) .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) + let saved_comments = comment_actions::table + .filter(comment_actions::saved.is_not_null()) .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) + let blocked_communities = community_actions::table + .filter(community_actions::blocked.is_not_null()) .filter(community_actions::person_id.eq(person_id_)) .inner_join(community::table) - .select(community::actor_id) + .select(community::ap_id) .get_results(conn) .await?; - let blocked_users = action_query(person_actions::blocked) + let blocked_users = person_actions::table + .filter(person_actions::blocked.is_not_null()) .filter(person_actions::person_id.eq(person_id_)) .inner_join(person::table.on(person_actions::target_id.eq(person::id))) - .select(person::actor_id) + .select(person::ap_id) .get_results(conn) .await?; - let blocked_instances = action_query(instance_actions::blocked) + let blocked_instances = instance_actions::table + .filter(instance_actions::blocked.is_not_null()) .filter(instance_actions::person_id.eq(person_id_)) .inner_join(instance::table) .select(instance::domain) @@ -271,7 +278,8 @@ impl LocalUser { .order_by(local_user::id) .select(local_user::person_id); - let mods = action_query(community_actions::became_moderator) + let mods = community_actions::table + .filter(community_actions::became_moderator.is_not_null()) .filter(community_actions::community_id.eq(for_community_id)) .filter(community_actions::person_id.eq_any(&persons)) .order_by(community_actions::became_moderator) @@ -287,6 +295,33 @@ impl LocalUser { Err(LemmyErrorType::NotHigherMod)? } } + + pub fn interface_i18n_language(&self) -> Lang { + lang_str_to_lang(&self.interface_language) + } +} + +// TODO +// I'd really like to have these on the impl, but unfortunately they have to be top level, +// according to https://diesel.rs/guides/composing-applications.html +/// Checks to see if you can mod an item. +/// +/// Caveat: Since admin status isn't federated or ordered, it can't know whether +/// item creator is a federated admin, or a higher admin. +/// The back-end will reject an action for admin that is higher via +/// LocalUser::is_higher_mod_or_admin_check +#[diesel::dsl::auto_type] +pub fn local_user_can_mod() -> _ { + let am_admin = local_user::admin.nullable(); + let creator_became_moderator = creator_community_actions + .field(community_actions::became_moderator) + .nullable(); + + let am_higher_mod = community_actions::became_moderator + .nullable() + .le(creator_became_moderator); + + am_admin.or(am_higher_mod).is_not_distinct_from(true) } /// Adds some helper functions for an optional LocalUser diff --git a/crates/db_schema/src/impls/local_user_vote_display_mode.rs b/crates/db_schema/src/impls/local_user_vote_display_mode.rs deleted file mode 100644 index 2d169f81b..000000000 --- a/crates/db_schema/src/impls/local_user_vote_display_mode.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::{ - diesel::OptionalExtension, - newtypes::LocalUserId, - schema::local_user_vote_display_mode, - source::local_user_vote_display_mode::{ - LocalUserVoteDisplayMode, - LocalUserVoteDisplayModeInsertForm, - LocalUserVoteDisplayModeUpdateForm, - }, - utils::{get_conn, DbPool}, -}; -use diesel::{dsl::insert_into, result::Error, QueryDsl}; -use diesel_async::RunQueryDsl; - -impl LocalUserVoteDisplayMode { - pub async fn read(pool: &mut DbPool<'_>) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - local_user_vote_display_mode::table - .first(conn) - .await - .optional() - } - - pub async fn create( - pool: &mut DbPool<'_>, - form: &LocalUserVoteDisplayModeInsertForm, - ) -> Result { - let conn = &mut get_conn(pool).await?; - insert_into(local_user_vote_display_mode::table) - .values(form) - .get_result::(conn) - .await - } - - pub async fn update( - pool: &mut DbPool<'_>, - local_user_id: LocalUserId, - form: &LocalUserVoteDisplayModeUpdateForm, - ) -> Result<(), Error> { - // avoid error "There are no changes to save. This query cannot be built" - if form.is_empty() { - return Ok(()); - } - let conn = &mut get_conn(pool).await?; - diesel::update(local_user_vote_display_mode::table.find(local_user_id)) - .set(form) - .get_result::(conn) - .await?; - Ok(()) - } -} - -impl LocalUserVoteDisplayModeUpdateForm { - fn is_empty(&self) -> bool { - self.score.is_none() - && self.upvotes.is_none() - && self.downvotes.is_none() - && self.upvote_percentage.is_none() - } -} diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index f6a01f06a..d384afdd0 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -20,7 +20,6 @@ pub mod local_site; pub mod local_site_rate_limit; pub mod local_site_url_blocklist; pub mod local_user; -pub mod local_user_vote_display_mode; pub mod login_token; pub mod mod_log; pub mod oauth_account; @@ -31,6 +30,7 @@ pub mod person_block; pub mod person_comment_mention; pub mod person_post_mention; pub mod post; +pub mod post_actions; pub mod post_report; pub mod private_message; pub mod private_message_report; diff --git a/crates/db_schema/src/impls/mod_log/admin.rs b/crates/db_schema/src/impls/mod_log/admin.rs index 7d007ccba..e3b1cdb64 100644 --- a/crates/db_schema/src/impls/mod_log/admin.rs +++ b/crates/db_schema/src/impls/mod_log/admin.rs @@ -35,7 +35,6 @@ use crate::{ use diesel::{dsl::insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for AdminPurgePerson { type InsertForm = AdminPurgePersonForm; type UpdateForm = AdminPurgePersonForm; @@ -62,7 +61,6 @@ impl Crud for AdminPurgePerson { } } -#[async_trait] impl Crud for AdminPurgeCommunity { type InsertForm = AdminPurgeCommunityForm; type UpdateForm = AdminPurgeCommunityForm; @@ -89,7 +87,6 @@ impl Crud for AdminPurgeCommunity { } } -#[async_trait] impl Crud for AdminPurgePost { type InsertForm = AdminPurgePostForm; type UpdateForm = AdminPurgePostForm; @@ -116,7 +113,6 @@ impl Crud for AdminPurgePost { } } -#[async_trait] impl Crud for AdminPurgeComment { type InsertForm = AdminPurgeCommentForm; type UpdateForm = AdminPurgeCommentForm; @@ -143,7 +139,6 @@ impl Crud for AdminPurgeComment { } } -#[async_trait] impl Crud for AdminAllowInstance { type InsertForm = AdminAllowInstanceForm; type UpdateForm = AdminAllowInstanceForm; @@ -170,7 +165,6 @@ impl Crud for AdminAllowInstance { } } -#[async_trait] impl Crud for AdminBlockInstance { type InsertForm = AdminBlockInstanceForm; type UpdateForm = AdminBlockInstanceForm; diff --git a/crates/db_schema/src/impls/mod_log/moderator.rs b/crates/db_schema/src/impls/mod_log/moderator.rs index e95e2a3e3..233908c80 100644 --- a/crates/db_schema/src/impls/mod_log/moderator.rs +++ b/crates/db_schema/src/impls/mod_log/moderator.rs @@ -55,7 +55,6 @@ use crate::{ use diesel::{dsl::insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for ModRemovePost { type InsertForm = ModRemovePostForm; type UpdateForm = ModRemovePostForm; @@ -95,7 +94,6 @@ impl ModRemovePost { } } -#[async_trait] impl Crud for ModLockPost { type InsertForm = ModLockPostForm; type UpdateForm = ModLockPostForm; @@ -122,7 +120,6 @@ impl Crud for ModLockPost { } } -#[async_trait] impl Crud for ModFeaturePost { type InsertForm = ModFeaturePostForm; type UpdateForm = ModFeaturePostForm; @@ -149,7 +146,6 @@ impl Crud for ModFeaturePost { } } -#[async_trait] impl Crud for ModRemoveComment { type InsertForm = ModRemoveCommentForm; type UpdateForm = ModRemoveCommentForm; @@ -189,7 +185,6 @@ impl ModRemoveComment { } } -#[async_trait] impl Crud for ModRemoveCommunity { type InsertForm = ModRemoveCommunityForm; type UpdateForm = ModRemoveCommunityForm; @@ -216,7 +211,6 @@ impl Crud for ModRemoveCommunity { } } -#[async_trait] impl Crud for ModBanFromCommunity { type InsertForm = ModBanFromCommunityForm; type UpdateForm = ModBanFromCommunityForm; @@ -243,7 +237,6 @@ impl Crud for ModBanFromCommunity { } } -#[async_trait] impl Crud for ModBan { type InsertForm = ModBanForm; type UpdateForm = ModBanForm; @@ -270,7 +263,6 @@ impl Crud for ModBan { } } -#[async_trait] impl Crud for ModHideCommunity { type InsertForm = ModHideCommunityForm; type UpdateForm = ModHideCommunityForm; @@ -297,7 +289,6 @@ impl Crud for ModHideCommunity { } } -#[async_trait] impl Crud for ModAddCommunity { type InsertForm = ModAddCommunityForm; type UpdateForm = ModAddCommunityForm; @@ -324,7 +315,6 @@ impl Crud for ModAddCommunity { } } -#[async_trait] impl Crud for ModTransferCommunity { type InsertForm = ModTransferCommunityForm; type UpdateForm = ModTransferCommunityForm; @@ -351,7 +341,6 @@ impl Crud for ModTransferCommunity { } } -#[async_trait] impl Crud for ModAdd { type InsertForm = ModAddForm; type UpdateForm = ModAddForm; diff --git a/crates/db_schema/src/impls/oauth_provider.rs b/crates/db_schema/src/impls/oauth_provider.rs index 7665ba050..a9bee271f 100644 --- a/crates/db_schema/src/impls/oauth_provider.rs +++ b/crates/db_schema/src/impls/oauth_provider.rs @@ -13,7 +13,6 @@ use crate::{ use diesel::{dsl::insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for OAuthProvider { type InsertForm = OAuthProviderInsertForm; type UpdateForm = OAuthProviderUpdateForm; diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index fb8c96f04..9ea082a1d 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -10,7 +10,7 @@ use crate::{ PersonUpdateForm, }, traits::{ApubActor, Crud, Followable}, - utils::{action_query, functions::lower, get_conn, now, uplete, DbPool}, + utils::{functions::lower, get_conn, now, uplete, DbPool}, }; use chrono::Utc; use diesel::{ @@ -24,9 +24,12 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::{ + error::{LemmyErrorType, LemmyResult}, + settings::structs::Settings, +}; +use url::Url; -#[async_trait] impl Crud for Person { type InsertForm = PersonInsertForm; type UpdateForm = PersonUpdateForm; @@ -71,7 +74,7 @@ impl Person { let conn = &mut get_conn(pool).await?; insert_into(person::table) .values(form) - .on_conflict(person::actor_id) + .on_conflict(person::ap_id) .do_update() .set(form) .get_result::(conn) @@ -138,6 +141,11 @@ impl Person { .then_some(()) .ok_or(LemmyErrorType::UsernameAlreadyExists.into()) } + + pub fn local_url(name: &str, settings: &Settings) -> LemmyResult { + let domain = settings.get_protocol_and_hostname(); + Ok(Url::parse(&format!("{domain}/u/{name}"))?.into()) + } } impl PersonInsertForm { @@ -146,7 +154,6 @@ impl PersonInsertForm { } } -#[async_trait] impl ApubActor for Person { async fn read_from_apub_id( pool: &mut DbPool<'_>, @@ -155,7 +162,7 @@ impl ApubActor for Person { let conn = &mut get_conn(pool).await?; person::table .filter(person::deleted.eq(false)) - .filter(person::actor_id.eq(object_id)) + .filter(person::ap_id.eq(object_id)) .first(conn) .await .optional() @@ -195,7 +202,6 @@ impl ApubActor for Person { } } -#[async_trait] impl Followable for PersonFollower { type Form = PersonFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { @@ -235,7 +241,8 @@ impl PersonFollower { for_person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - action_query(person_actions::followed) + person_actions::table + .filter(person_actions::followed.is_not_null()) .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) @@ -249,12 +256,16 @@ mod tests { use crate::{ source::{ + comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, + community::{Community, CommunityInsertForm}, instance::Instance, person::{Person, PersonFollower, PersonFollowerForm, PersonInsertForm, PersonUpdateForm}, + post::{Post, PostInsertForm, PostLike, PostLikeForm}, }, - traits::{Crud, Followable}, + traits::{Crud, Followable, Likeable}, utils::{build_db_pool_for_tests, uplete}, }; + use diesel::result::Error; use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; @@ -281,7 +292,7 @@ mod tests { deleted: false, published: inserted_person.published, updated: None, - actor_id: inserted_person.actor_id.clone(), + ap_id: inserted_person.ap_id.clone(), bio: None, local: true, bot_account: false, @@ -292,12 +303,16 @@ mod tests { matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, + post_count: 0, + post_score: 0, + comment_count: 0, + comment_score: 0, }; let read_person = Person::read(pool, inserted_person.id).await?; let update_person_form = PersonUpdateForm { - actor_id: Some(inserted_person.actor_id.clone()), + ap_id: Some(inserted_person.ap_id.clone()), ..Default::default() }; let updated_person = Person::update(pool, inserted_person.id, &update_person_form).await?; @@ -343,4 +358,151 @@ mod tests { Ok(()) } + + #[tokio::test] + #[serial] + async fn test_aggregates() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_user_agg"); + + let inserted_person = Person::create(pool, &new_person).await?; + + let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_user_agg"); + + let another_inserted_person = Person::create(pool, &another_person).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_site_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + + let inserted_community = Community::create(pool, &new_community).await?; + + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; + + let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1); + let _inserted_post_like = PostLike::like(pool, &post_like).await?; + + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + let mut comment_like = CommentLikeForm { + comment_id: inserted_comment.id, + person_id: inserted_person.id, + score: 1, + }; + + let _inserted_comment_like = CommentLike::like(pool, &comment_like).await?; + + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_child_comment = + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; + + let child_comment_like = CommentLikeForm { + comment_id: inserted_child_comment.id, + person_id: another_inserted_person.id, + score: 1, + }; + + let _inserted_child_comment_like = CommentLike::like(pool, &child_comment_like).await?; + + let person_aggregates_before_delete = Person::read(pool, inserted_person.id).await?; + + assert_eq!(1, person_aggregates_before_delete.post_count); + assert_eq!(1, person_aggregates_before_delete.post_score); + assert_eq!(2, person_aggregates_before_delete.comment_count); + assert_eq!(2, person_aggregates_before_delete.comment_score); + + // Remove a post like + PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; + let after_post_like_remove = Person::read(pool, inserted_person.id).await?; + assert_eq!(0, after_post_like_remove.post_score); + + Comment::update( + pool, + inserted_comment.id, + &CommentUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + Comment::update( + pool, + inserted_child_comment.id, + &CommentUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + + let after_parent_comment_removed = Person::read(pool, inserted_person.id).await?; + assert_eq!(0, after_parent_comment_removed.comment_count); + // TODO: fix person aggregate comment score calculation + // assert_eq!(0, after_parent_comment_removed.comment_score); + + // Remove a parent comment (the scores should also be removed) + Comment::delete(pool, inserted_comment.id).await?; + Comment::delete(pool, inserted_child_comment.id).await?; + let after_parent_comment_delete = Person::read(pool, inserted_person.id).await?; + assert_eq!(0, after_parent_comment_delete.comment_count); + // TODO: fix person aggregate comment score calculation + // assert_eq!(0, after_parent_comment_delete.comment_score); + + // Add in the two comments again, then delete the post. + let new_parent_comment = Comment::create(pool, &comment_form, None).await?; + let _new_child_comment = + Comment::create(pool, &child_comment_form, Some(&new_parent_comment.path)).await?; + comment_like.comment_id = new_parent_comment.id; + CommentLike::like(pool, &comment_like).await?; + let after_comment_add = Person::read(pool, inserted_person.id).await?; + assert_eq!(2, after_comment_add.comment_count); + // TODO: fix person aggregate comment score calculation + // assert_eq!(1, after_comment_add.comment_score); + + Post::delete(pool, inserted_post.id).await?; + let after_post_delete = Person::read(pool, inserted_person.id).await?; + // TODO: fix person aggregate comment score calculation + // assert_eq!(0, after_post_delete.comment_score); + assert_eq!(0, after_post_delete.comment_count); + assert_eq!(0, after_post_delete.post_score); + assert_eq!(0, after_post_delete.post_count); + + // This should delete all the associated rows, and fire triggers + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; + assert_eq!(1, person_num_deleted); + Person::delete(pool, another_inserted_person.id).await?; + + // Delete the community + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; + assert_eq!(1, community_num_deleted); + + // Should be none found + let after_delete = Person::read(pool, inserted_person.id).await; + assert!(after_delete.is_err()); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } } diff --git a/crates/db_schema/src/impls/person_block.rs b/crates/db_schema/src/impls/person_block.rs index 363a2d3d1..db0b3c86f 100644 --- a/crates/db_schema/src/impls/person_block.rs +++ b/crates/db_schema/src/impls/person_block.rs @@ -6,7 +6,7 @@ use crate::{ person_block::{PersonBlock, PersonBlockForm}, }, traits::Blockable, - utils::{action_query, find_action, get_conn, now, uplete, DbPool}, + utils::{get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, @@ -28,14 +28,14 @@ impl PersonBlock { for_recipient_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists(find_action( - person_actions::blocked, - (for_person_id, for_recipient_id), - )))) - .get_result::(conn) - .await? - .then_some(()) - .ok_or(LemmyErrorType::PersonIsBlocked.into()) + let find_action = person_actions::table + .find((for_person_id, for_recipient_id)) + .filter(person_actions::blocked.is_not_null()); + select(not(exists(find_action))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::PersonIsBlocked.into()) } pub async fn for_person( @@ -45,7 +45,8 @@ impl PersonBlock { let conn = &mut get_conn(pool).await?; let target_person_alias = diesel::alias!(person as person1); - action_query(person_actions::blocked) + person_actions::table + .filter(person_actions::blocked.is_not_null()) .inner_join(person::table.on(person_actions::person_id.eq(person::id))) .inner_join( target_person_alias.on(person_actions::target_id.eq(target_person_alias.field(person::id))), @@ -59,7 +60,6 @@ impl PersonBlock { } } -#[async_trait] impl Blockable for PersonBlock { type Form = PersonBlockForm; async fn block( diff --git a/crates/db_schema/src/impls/person_comment_mention.rs b/crates/db_schema/src/impls/person_comment_mention.rs index 2cc303396..03af4cc46 100644 --- a/crates/db_schema/src/impls/person_comment_mention.rs +++ b/crates/db_schema/src/impls/person_comment_mention.rs @@ -13,7 +13,6 @@ use crate::{ use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for PersonCommentMention { type InsertForm = PersonCommentMentionInsertForm; type UpdateForm = PersonCommentMentionUpdateForm; diff --git a/crates/db_schema/src/impls/person_post_mention.rs b/crates/db_schema/src/impls/person_post_mention.rs index ef59b60e1..522c5b966 100644 --- a/crates/db_schema/src/impls/person_post_mention.rs +++ b/crates/db_schema/src/impls/person_post_mention.rs @@ -13,7 +13,6 @@ use crate::{ use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for PersonPostMention { type InsertForm = PersonPostMentionInsertForm; type UpdateForm = PersonPostMentionUpdateForm; diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index 96818ec6d..1e4dac6de 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -1,5 +1,4 @@ use crate::{ - diesel::{BoolExpressionMethods, NullableExpressionMethods, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId, PostId}, schema::{community, person, post, post_actions}, source::post::{ @@ -18,10 +17,11 @@ use crate::{ }, traits::{Crud, Likeable, Saveable}, utils::{ - functions::coalesce, + functions::{coalesce, hot_rank, scaled_rank}, get_conn, now, uplete, + DbConn, DbPool, DELETED_REPLACEMENT_TEXT, FETCH_LIMIT_MAX, @@ -35,15 +35,21 @@ use diesel::{ dsl::{count, insert_into, not}, expression::SelectableHelper, result::Error, + BoolExpressionMethods, DecoratableTarget, ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + OptionalExtension, QueryDsl, TextExpressionMethods, }; use diesel_async::RunQueryDsl; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::{ + error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, + settings::structs::Settings, +}; -#[async_trait] impl Crud for Post { type InsertForm = PostInsertForm; type UpdateForm = PostUpdateForm; @@ -180,6 +186,19 @@ impl Post { .optional() } + pub async fn delete_from_apub_id( + pool: &mut DbPool<'_>, + object_id: Url, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let object_id: DbUrl = object_id.into(); + + diesel::update(post::table.filter(post::ap_id.eq(object_id))) + .set(post::deleted.eq(true)) + .get_results::(conn) + .await + } + pub async fn fetch_pictrs_posts_for_creator( pool: &mut DbPool<'_>, for_creator_id: PersonId, @@ -270,9 +289,40 @@ impl Post { .first::(conn) .await } + + pub async fn update_ranks(pool: &mut DbPool<'_>, post_id: PostId) -> Result { + let conn = &mut get_conn(pool).await?; + + // Diesel can't update based on a join, which is necessary for the scaled_rank + // https://github.com/diesel-rs/diesel/issues/1478 + // Just select the metrics we need manually, for now, since its a single post anyway + + let interactions_month = community::table + .select(community::interactions_month) + .inner_join(post::table.on(community::id.eq(post::community_id))) + .filter(post::id.eq(post_id)) + .first::(conn) + .await?; + + diesel::update(post::table.find(post_id)) + .set(( + post::hot_rank.eq(hot_rank(post::score, post::published)), + post::hot_rank_active.eq(hot_rank(post::score, post::newest_comment_time_necro)), + post::scaled_rank.eq(scaled_rank( + post::score, + post::published, + interactions_month, + )), + )) + .get_result::(conn) + .await + } + pub fn local_url(&self, settings: &Settings) -> LemmyResult { + let domain = settings.get_protocol_and_hostname(); + Ok(Url::parse(&format!("{domain}/post/{}", self.id))?.into()) + } } -#[async_trait] impl Likeable for PostLike { type Form = PostLikeForm; type IdType = PostId; @@ -301,7 +351,6 @@ impl Likeable for PostLike { } } -#[async_trait] impl Saveable for PostSaved { type Form = PostSavedForm; async fn save(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { @@ -418,6 +467,14 @@ impl PostActionsCursor { ) -> Result { let conn = &mut get_conn(pool).await?; + Self::read_conn(conn, post_id, person_id).await + } + + pub async fn read_conn( + conn: &mut DbConn<'_>, + post_id: PostId, + person_id: Option, + ) -> Result { Ok(if let Some(person_id) = person_id { post_actions::table .find((person_id, post_id)) @@ -434,9 +491,9 @@ impl PostActionsCursor { #[cfg(test)] mod tests { - use crate::{ source::{ + comment::{Comment, CommentInsertForm, CommentUpdateForm}, community::{Community, CommunityInsertForm}, instance::Instance, person::{Person, PersonInsertForm}, @@ -453,9 +510,10 @@ mod tests { }, }, traits::{Crud, Likeable, Saveable}, - utils::{build_db_pool_for_tests, uplete}, + utils::{build_db_pool_for_tests, uplete, RANK_DEFAULT}, }; use chrono::DateTime; + use diesel::result::Error; use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; @@ -527,6 +585,19 @@ mod tests { featured_local: false, url_content_type: None, scheduled_publish_time: None, + comments: 0, + controversy_rank: 0.0, + downvotes: 0, + upvotes: 1, + score: 1, + hot_rank: RANK_DEFAULT, + hot_rank_active: RANK_DEFAULT, + instance_id: inserted_instance.id, + newest_comment_time: inserted_post.published, + newest_comment_time_necro: inserted_post.published, + report_count: 0, + scaled_rank: RANK_DEFAULT, + unresolved_report_count: 0, }; // Post Like @@ -593,11 +664,210 @@ mod tests { Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_post, read_post); - assert_eq!(expected_post, inserted_post); assert_eq!(expected_post, updated_post); assert_eq!(expected_post_like, inserted_post_like); assert_eq!(expected_post_saved, inserted_post_saved); Ok(()) } + + #[tokio::test] + #[serial] + async fn test_aggregates() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); + + let inserted_person = Person::create(pool, &new_person).await?; + + let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); + + let another_inserted_person = Person::create(pool, &another_person).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_community_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; + + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; + + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + let child_comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + let inserted_child_comment = + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; + + let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1); + + PostLike::like(pool, &post_like).await?; + + let post_aggs_before_delete = Post::read(pool, inserted_post.id).await?; + + assert_eq!(2, post_aggs_before_delete.comments); + assert_eq!(1, post_aggs_before_delete.score); + assert_eq!(1, post_aggs_before_delete.upvotes); + assert_eq!(0, post_aggs_before_delete.downvotes); + + // Add a post dislike from the other person + let post_dislike = PostLikeForm::new(inserted_post.id, another_inserted_person.id, -1); + + PostLike::like(pool, &post_dislike).await?; + + let post_aggs_after_dislike = Post::read(pool, inserted_post.id).await?; + + assert_eq!(2, post_aggs_after_dislike.comments); + assert_eq!(0, post_aggs_after_dislike.score); + assert_eq!(1, post_aggs_after_dislike.upvotes); + assert_eq!(1, post_aggs_after_dislike.downvotes); + + // Remove the comments + Comment::delete(pool, inserted_comment.id).await?; + Comment::delete(pool, inserted_child_comment.id).await?; + let after_comment_delete = Post::read(pool, inserted_post.id).await?; + assert_eq!(0, after_comment_delete.comments); + assert_eq!(0, after_comment_delete.score); + assert_eq!(1, after_comment_delete.upvotes); + assert_eq!(1, after_comment_delete.downvotes); + + // Remove the first post like + PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; + let after_like_remove = Post::read(pool, inserted_post.id).await?; + assert_eq!(0, after_like_remove.comments); + assert_eq!(-1, after_like_remove.score); + assert_eq!(0, after_like_remove.upvotes); + assert_eq!(1, after_like_remove.downvotes); + + // This should delete all the associated rows, and fire triggers + Person::delete(pool, another_inserted_person.id).await?; + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; + assert_eq!(1, person_num_deleted); + + // Delete the community + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; + assert_eq!(1, community_num_deleted); + + // Should be none found, since the creator was deleted + let after_delete = Post::read(pool, inserted_post.id).await; + assert!(after_delete.is_err()); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_aggregates_soft_delete() -> Result<(), Error> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); + + let inserted_person = Person::create(pool, &new_person).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "TIL_community_agg".into(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; + + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; + + let comment_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "A test comment".into(), + ); + + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + let post_aggregates_before = Post::read(pool, inserted_post.id).await?; + assert_eq!(1, post_aggregates_before.comments); + + Comment::update( + pool, + inserted_comment.id, + &CommentUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + + let post_aggregates_after_remove = Post::read(pool, inserted_post.id).await?; + assert_eq!(0, post_aggregates_after_remove.comments); + + Comment::update( + pool, + inserted_comment.id, + &CommentUpdateForm { + removed: Some(false), + ..Default::default() + }, + ) + .await?; + + Comment::update( + pool, + inserted_comment.id, + &CommentUpdateForm { + deleted: Some(true), + ..Default::default() + }, + ) + .await?; + + let post_aggregates_after_delete = Post::read(pool, inserted_post.id).await?; + assert_eq!(0, post_aggregates_after_delete.comments); + + Comment::update( + pool, + inserted_comment.id, + &CommentUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + + let post_aggregates_after_delete_remove = Post::read(pool, inserted_post.id).await?; + assert_eq!(0, post_aggregates_after_delete_remove.comments); + + Comment::delete(pool, inserted_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Person::delete(pool, inserted_person.id).await?; + Community::delete(pool, inserted_community.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } } diff --git a/crates/db_schema/src/aggregates/person_post_aggregates.rs b/crates/db_schema/src/impls/post_actions.rs similarity index 73% rename from crates/db_schema/src/aggregates/person_post_aggregates.rs rename to crates/db_schema/src/impls/post_actions.rs index 63a50af9c..ac3ef22e7 100644 --- a/crates/db_schema/src/aggregates/person_post_aggregates.rs +++ b/crates/db_schema/src/impls/post_actions.rs @@ -1,9 +1,9 @@ use crate::{ - aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, diesel::OptionalExtension, newtypes::{PersonId, PostId}, schema::post_actions, - utils::{find_action, get_conn, now, DbPool}, + source::post_actions::{PostActions, PostActionsForm}, + utils::{get_conn, now, DbPool}, }; use diesel::{ expression::SelectableHelper, @@ -15,11 +15,8 @@ use diesel::{ }; use diesel_async::RunQueryDsl; -impl PersonPostAggregates { - pub async fn upsert( - pool: &mut DbPool<'_>, - form: &PersonPostAggregatesForm, - ) -> Result { +impl PostActions { + pub async fn upsert(pool: &mut DbPool<'_>, form: &PostActionsForm) -> Result { let conn = &mut get_conn(pool).await?; let form = (form, post_actions::read_comments.eq(now().nullable())); insert_into(post_actions::table) @@ -37,7 +34,9 @@ impl PersonPostAggregates { post_id_: PostId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - find_action(post_actions::read_comments, (person_id_, post_id_)) + post_actions::table + .find((person_id_, post_id_)) + .filter(post_actions::read_comments.is_not_null()) .select(Self::as_select()) .first(conn) .await diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index 90ac030c1..bcc61691a 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -1,9 +1,7 @@ use crate::{ + diesel::BoolExpressionMethods, newtypes::{PersonId, PostId, PostReportId}, - schema::post_report::{ - dsl::{post_report, resolved, resolver_id, updated}, - post_id, - }, + schema::post_report, source::post_report::{PostReport, PostReportForm}, traits::Reportable, utils::{get_conn, DbPool}, @@ -16,8 +14,8 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; -#[async_trait] impl Reportable for PostReport { type Form = PostReportForm; type IdType = PostReportId; @@ -25,7 +23,7 @@ impl Reportable for PostReport { async fn report(pool: &mut DbPool<'_>, post_report_form: &PostReportForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post_report) + insert_into(post_report::table) .values(post_report_form) .get_result::(conn) .await @@ -37,27 +35,52 @@ impl Reportable for PostReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(post_report.find(report_id)) + update(post_report::table.find(report_id)) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + post_report::resolved.eq(true), + post_report::resolver_id.eq(by_resolver_id), + post_report::updated.eq(Utc::now()), )) .execute(conn) .await } + async fn resolve_apub( + pool: &mut DbPool<'_>, + object_id: Self::ObjectIdType, + report_creator_id: PersonId, + resolver_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + Ok( + update( + post_report::table.filter( + post_report::post_id + .eq(object_id) + .and(post_report::creator_id.eq(report_creator_id)), + ), + ) + .set(( + post_report::resolved.eq(true), + post_report::resolver_id.eq(resolver_id), + post_report::updated.eq(Utc::now()), + )) + .execute(conn) + .await?, + ) + } + async fn resolve_all_for_object( pool: &mut DbPool<'_>, post_id_: PostId, by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(post_report.filter(post_id.eq(post_id_))) + update(post_report::table.filter(post_report::post_id.eq(post_id_))) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + post_report::resolved.eq(true), + post_report::resolver_id.eq(by_resolver_id), + post_report::updated.eq(Utc::now()), )) .execute(conn) .await @@ -69,11 +92,11 @@ impl Reportable for PostReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(post_report.find(report_id)) + update(post_report::table.find(report_id)) .set(( - resolved.eq(false), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + post_report::resolved.eq(false), + post_report::resolver_id.eq(by_resolver_id), + post_report::updated.eq(Utc::now()), )) .execute(conn) .await diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index e08b4cf7f..7a6d67611 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -9,9 +9,9 @@ use crate::{ use chrono::{DateTime, Utc}; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; +use lemmy_utils::{error::LemmyResult, settings::structs::Settings}; use url::Url; -#[async_trait] impl Crud for PrivateMessage { type InsertForm = PrivateMessageInsertForm; type UpdateForm = PrivateMessageUpdateForm; @@ -82,6 +82,25 @@ impl PrivateMessage { .await .optional() } + pub fn local_url(&self, settings: &Settings) -> LemmyResult { + let domain = settings.get_protocol_and_hostname(); + Ok(Url::parse(&format!("{domain}/private_message/{}", self.id))?.into()) + } + + pub async fn update_removed_for_creator( + pool: &mut DbPool<'_>, + for_creator_id: PersonId, + removed: bool, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + diesel::update(private_message::table.filter(private_message::creator_id.eq(for_creator_id))) + .set(( + private_message::removed.eq(removed), + private_message::updated.eq(Utc::now()), + )) + .get_results::(conn) + .await + } } #[cfg(test)] @@ -140,6 +159,7 @@ mod tests { ))? .into(), local: true, + removed: false, }; let read_private_message = PrivateMessage::read(pool, inserted_private_message.id).await?; diff --git a/crates/db_schema/src/impls/private_message_report.rs b/crates/db_schema/src/impls/private_message_report.rs index 0a83bf637..3366f2fbf 100644 --- a/crates/db_schema/src/impls/private_message_report.rs +++ b/crates/db_schema/src/impls/private_message_report.rs @@ -13,8 +13,8 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{FederationError, LemmyResult}; -#[async_trait] impl Reportable for PrivateMessageReport { type Form = PrivateMessageReportForm; type IdType = PrivateMessageReportId; @@ -46,6 +46,14 @@ impl Reportable for PrivateMessageReport { .execute(conn) .await } + async fn resolve_apub( + _pool: &mut DbPool<'_>, + _object_id: Self::ObjectIdType, + _report_creator_id: PersonId, + _resolver_id: PersonId, + ) -> LemmyResult { + Err(FederationError::Unreachable.into()) + } // TODO: this is unused because private message doesn't have remove handler async fn resolve_all_for_object( diff --git a/crates/db_schema/src/impls/registration_application.rs b/crates/db_schema/src/impls/registration_application.rs index d9777919d..23341cbb1 100644 --- a/crates/db_schema/src/impls/registration_application.rs +++ b/crates/db_schema/src/impls/registration_application.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{LocalUserId, RegistrationApplicationId}, - schema::registration_application::dsl::{local_user_id, registration_application}, + schema::registration_application, source::registration_application::{ RegistrationApplication, RegistrationApplicationInsertForm, @@ -12,7 +12,6 @@ use crate::{ use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for RegistrationApplication { type InsertForm = RegistrationApplicationInsertForm; type UpdateForm = RegistrationApplicationUpdateForm; @@ -20,7 +19,7 @@ impl Crud for RegistrationApplication { async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(registration_application) + insert_into(registration_application::table) .values(form) .get_result::(conn) .await @@ -32,7 +31,7 @@ impl Crud for RegistrationApplication { form: &Self::UpdateForm, ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(registration_application.find(id_)) + diesel::update(registration_application::table.find(id_)) .set(form) .get_result::(conn) .await @@ -45,9 +44,15 @@ impl RegistrationApplication { local_user_id_: LocalUserId, ) -> Result { let conn = &mut get_conn(pool).await?; - registration_application - .filter(local_user_id.eq(local_user_id_)) + registration_application::table + .filter(registration_application::local_user_id.eq(local_user_id_)) .first(conn) .await } + + /// A missing admin id, means the application is unread + #[diesel::dsl::auto_type(no_type_alias)] + pub fn is_unread() -> _ { + registration_application::admin_id.is_null() + } } diff --git a/crates/db_schema/src/impls/site.rs b/crates/db_schema/src/impls/site.rs index 7ab13b8e2..5982642ee 100644 --- a/crates/db_schema/src/impls/site.rs +++ b/crates/db_schema/src/impls/site.rs @@ -13,7 +13,6 @@ use diesel_async::RunQueryDsl; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use url::Url; -#[async_trait] impl Crud for Site { type InsertForm = SiteInsertForm; type UpdateForm = SiteUpdateForm; @@ -25,7 +24,7 @@ impl Crud for Site { } async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - let is_new_site = match &form.actor_id { + let is_new_site = match &form.ap_id { Some(id_) => Site::read_from_apub_id(pool, id_).await?.is_none(), None => true, }; @@ -34,7 +33,7 @@ impl Crud for Site { // Can't do separate insert/update commands because InsertForm/UpdateForm aren't convertible let site_ = insert_into(site::table) .values(form) - .on_conflict(site::actor_id) + .on_conflict(site::ap_id) .do_update() .set(form) .get_result::(conn) @@ -80,7 +79,7 @@ impl Site { let conn = &mut get_conn(pool).await?; site::table - .filter(site::actor_id.eq(object_id)) + .filter(site::ap_id.eq(object_id)) .first(conn) .await .optional() @@ -97,7 +96,7 @@ impl Site { /// Instance actor is at the root path, so we simply need to clear the path and other unnecessary /// parts of the url. - pub fn instance_actor_id_from_url(mut url: Url) -> Url { + pub fn instance_ap_id_from_url(mut url: Url) -> Url { url.set_fragment(None); url.set_path(""); url.set_query(None); diff --git a/crates/db_schema/src/impls/tag.rs b/crates/db_schema/src/impls/tag.rs index c0171e04c..a6d6b41f7 100644 --- a/crates/db_schema/src/impls/tag.rs +++ b/crates/db_schema/src/impls/tag.rs @@ -9,7 +9,6 @@ use diesel::{insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_utils::error::LemmyResult; -#[async_trait] impl Crud for Tag { type InsertForm = TagInsertForm; diff --git a/crates/db_schema/src/impls/tagline.rs b/crates/db_schema/src/impls/tagline.rs index ed9f82538..38671fe26 100644 --- a/crates/db_schema/src/impls/tagline.rs +++ b/crates/db_schema/src/impls/tagline.rs @@ -8,7 +8,6 @@ use crate::{ use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -#[async_trait] impl Crud for Tagline { type InsertForm = TaglineInsertForm; type UpdateForm = TaglineUpdateForm; diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index c077f758b..e82883b17 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -1,5 +1,3 @@ -#![recursion_limit = "256"] - #[cfg(feature = "full")] #[macro_use] extern crate diesel; @@ -11,11 +9,6 @@ extern crate diesel_derive_newtype; #[macro_use] extern crate diesel_derive_enum; -#[cfg(feature = "full")] -#[macro_use] -extern crate async_trait; - -pub mod aggregates; #[cfg(feature = "full")] pub mod impls; pub mod newtypes; @@ -25,9 +18,10 @@ pub mod sensitive; pub mod schema; #[cfg(feature = "full")] pub mod aliases { - use crate::schema::{community_actions, person}; + use crate::schema::{community_actions, local_user, person}; diesel::alias!( community_actions as creator_community_actions: CreatorCommunityActions, + local_user as creator_local_user: CreatorLocalUser, person as person1: Person1, person as person2: Person2, ); @@ -44,7 +38,7 @@ pub mod schema_setup; use serde::{Deserialize, Serialize}; use strum::{Display, EnumString}; #[cfg(feature = "full")] -use ts_rs::TS; +use {diesel::query_source::AliasedField, schema::person, ts_rs::TS}; #[derive( EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash, @@ -64,19 +58,9 @@ pub enum PostSortType { Hot, New, Old, - TopDay, - TopWeek, - TopMonth, - TopYear, - TopAll, + Top, MostComments, NewComments, - TopHour, - TopSixHour, - TopTwelveHour, - TopThreeMonths, - TopSixMonths, - TopNineMonths, Controversial, Scaled, } @@ -101,6 +85,19 @@ pub enum CommentSortType { Controversial, } +#[derive( + EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash, +)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The search sort types. +pub enum SearchSortType { + #[default] + New, + Top, + Old, +} + #[derive( EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash, )] @@ -166,11 +163,14 @@ pub enum PostListingMode { SmallCard, } -#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +#[derive( + EnumString, Display, Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Hash, +)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// The type of content returned from a search. pub enum SearchType { + #[default] All, Comments, Posts, @@ -229,13 +229,25 @@ pub enum InboxDataType { #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// A list of possible types for the various modlog actions. +/// A list of possible types for a person's content. pub enum PersonContentType { All, Comments, Posts, } +#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A list of possible types for reports. +pub enum ReportType { + All, + Posts, + Comments, + PrivateMessages, + Communities, +} + #[derive( EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash, )] @@ -292,13 +304,6 @@ pub enum FederationMode { Disable, } -pub trait InternalToCombinedView { - type CombinedView; - - /// Maps the combined DB row to an enum - fn map_to_enum(self) -> Option; -} - /// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the /// vec on failure. #[macro_export] @@ -307,3 +312,32 @@ macro_rules! assert_length { assert_eq!($len, $vec.len(), "Vec has wrong length: {:?}", $vec) }}; } + +#[cfg(feature = "full")] +/// A helper tuple for person alias columns +pub type Person1AliasAllColumnsTuple = ( + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, + AliasedField, +); diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 66a02a79b..7e498d9ce 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -15,6 +15,8 @@ use diesel::{ }; #[cfg(feature = "full")] use diesel_ltree::Ltree; +#[cfg(feature = "full")] +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; use std::{ fmt, @@ -220,6 +222,11 @@ pub struct ModlogCombinedId(i32); /// The inbox combined id pub struct InboxCombinedId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +/// The search combined id +pub struct SearchCombinedId(i32); + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] @@ -415,3 +422,31 @@ impl InstanceId { #[cfg_attr(feature = "full", ts(export))] /// The internal tag id. pub struct TagId(pub i32); + +/// A pagination cursor +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct PaginationCursor(pub String); + +#[cfg(feature = "full")] +impl PaginationCursor { + pub fn new(prefix: char, id: i32) -> Self { + // hex encoding to prevent ossification + Self(format!("{prefix}{id:x}")) + } + + pub fn prefix_and_id(&self) -> LemmyResult<(char, i32)> { + let (prefix_str, id_str) = self + .0 + .split_at_checked(1) + .ok_or(LemmyErrorType::CouldntParsePaginationToken)?; + let prefix = prefix_str + .chars() + .next() + .ok_or(LemmyErrorType::CouldntParsePaginationToken)?; + let id = i32::from_str_radix(id_str, 16)?; + + Ok((prefix, id)) + } +} diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 80a1b4c6f..49693c99c 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -130,6 +130,14 @@ diesel::table! { path -> Ltree, distinguished -> Bool, language_id -> Int4, + score -> Int8, + upvotes -> Int8, + downvotes -> Int8, + child_count -> Int4, + hot_rank -> Float8, + controversy_rank -> Float8, + report_count -> Int2, + unresolved_report_count -> Int2, } } @@ -143,21 +151,6 @@ diesel::table! { } } -diesel::table! { - comment_aggregates (comment_id) { - comment_id -> Int4, - score -> Int8, - upvotes -> Int8, - downvotes -> Int8, - published -> Timestamptz, - child_count -> Int4, - hot_rank -> Float8, - controversy_rank -> Float8, - report_count -> Int2, - unresolved_report_count -> Int2, - } -} - diesel::table! { comment_reply (id) { id -> Int4, @@ -179,6 +172,7 @@ diesel::table! { resolver_id -> Nullable, published -> Timestamptz, updated -> Nullable, + violates_instance_rules -> Bool, } } @@ -199,7 +193,7 @@ diesel::table! { deleted -> Bool, nsfw -> Bool, #[max_length = 255] - actor_id -> Varchar, + ap_id -> Varchar, local -> Bool, private_key -> Nullable, public_key -> Text, @@ -221,6 +215,18 @@ diesel::table! { #[max_length = 150] description -> Nullable, random_number -> Int2, + subscribers -> Int8, + posts -> Int8, + comments -> Int8, + users_active_day -> Int8, + users_active_week -> Int8, + users_active_month -> Int8, + users_active_half_year -> Int8, + hot_rank -> Float8, + subscribers_local -> Int8, + report_count -> Int2, + unresolved_report_count -> Int2, + interactions_month -> Int8, } } @@ -241,24 +247,6 @@ diesel::table! { } } -diesel::table! { - community_aggregates (community_id) { - community_id -> Int4, - subscribers -> Int8, - posts -> Int8, - comments -> Int8, - published -> Timestamptz, - users_active_day -> Int8, - users_active_week -> Int8, - users_active_month -> Int8, - users_active_half_year -> Int8, - hot_rank -> Float8, - subscribers_local -> Int8, - report_count -> Int2, - unresolved_report_count -> Int2, - } -} - diesel::table! { community_language (community_id, language_id) { community_id -> Int4, @@ -349,6 +337,8 @@ diesel::table! { width -> Int4, height -> Int4, content_type -> Text, + #[max_length = 50] + blurhash -> Nullable, } } @@ -444,6 +434,16 @@ diesel::table! { comment_upvotes -> FederationModeEnum, comment_downvotes -> FederationModeEnum, disable_donation_dialog -> Bool, + default_post_time_range_seconds -> Nullable, + users -> Int8, + posts -> Int8, + comments -> Int8, + communities -> Int8, + users_active_day -> Int8, + users_active_week -> Int8, + users_active_month -> Int8, + users_active_half_year -> Int8, + disallow_nsfw_content -> Bool, } } @@ -517,6 +517,11 @@ diesel::table! { auto_mark_fetched_posts_as_read -> Bool, last_donation_notification -> Timestamptz, hide_media -> Bool, + default_post_time_range_seconds -> Nullable, + show_score -> Bool, + show_upvotes -> Bool, + show_downvotes -> Bool, + show_upvote_percentage -> Bool, } } @@ -527,16 +532,6 @@ diesel::table! { } } -diesel::table! { - local_user_vote_display_mode (local_user_id) { - local_user_id -> Int4, - score -> Bool, - upvotes -> Bool, - downvotes -> Bool, - upvote_percentage -> Bool, - } -} - diesel::table! { login_token (token) { token -> Text, @@ -744,7 +739,7 @@ diesel::table! { published -> Timestamptz, updated -> Nullable, #[max_length = 255] - actor_id -> Varchar, + ap_id -> Varchar, bio -> Nullable, local -> Bool, private_key -> Nullable, @@ -758,6 +753,10 @@ diesel::table! { bot_account -> Bool, ban_expires -> Nullable, instance_id -> Int4, + post_count -> Int8, + post_score -> Int8, + comment_count -> Int8, + comment_score -> Int8, } } @@ -771,16 +770,6 @@ diesel::table! { } } -diesel::table! { - person_aggregates (person_id) { - person_id -> Int4, - post_count -> Int8, - post_score -> Int8, - comment_count -> Int8, - comment_score -> Int8, - } -} - diesel::table! { person_ban (person_id) { person_id -> Int4, @@ -856,6 +845,19 @@ diesel::table! { url_content_type -> Nullable, alt_text -> Nullable, scheduled_publish_time -> Nullable, + comments -> Int8, + score -> Int8, + upvotes -> Int8, + downvotes -> Int8, + newest_comment_time_necro -> Timestamptz, + newest_comment_time -> Timestamptz, + hot_rank -> Float8, + hot_rank_active -> Float8, + controversy_rank -> Float8, + instance_id -> Int4, + scaled_rank -> Float8, + report_count -> Int2, + unresolved_report_count -> Int2, } } @@ -873,30 +875,6 @@ diesel::table! { } } -diesel::table! { - post_aggregates (post_id) { - post_id -> Int4, - comments -> Int8, - score -> Int8, - upvotes -> Int8, - downvotes -> Int8, - published -> Timestamptz, - newest_comment_time_necro -> Timestamptz, - newest_comment_time -> Timestamptz, - featured_community -> Bool, - featured_local -> Bool, - hot_rank -> Float8, - hot_rank_active -> Float8, - community_id -> Int4, - creator_id -> Int4, - controversy_rank -> Float8, - instance_id -> Int4, - scaled_rank -> Float8, - report_count -> Int2, - unresolved_report_count -> Int2, - } -} - diesel::table! { post_report (id) { id -> Int4, @@ -911,6 +889,7 @@ diesel::table! { resolver_id -> Nullable, published -> Timestamptz, updated -> Nullable, + violates_instance_rules -> Bool, } } @@ -942,6 +921,7 @@ diesel::table! { #[max_length = 255] ap_id -> Varchar, local -> Bool, + removed -> Bool, } } @@ -995,6 +975,18 @@ diesel::table! { } } +diesel::table! { + search_combined (id) { + id -> Int4, + published -> Timestamptz, + score -> Int8, + post_id -> Nullable, + comment_id -> Nullable, + community_id -> Nullable, + person_id -> Nullable, + } +} + diesel::table! { secret (id) { id -> Int4, @@ -1033,7 +1025,7 @@ diesel::table! { #[max_length = 150] description -> Nullable, #[max_length = 255] - actor_id -> Varchar, + ap_id -> Varchar, last_refreshed_at -> Timestamptz, #[max_length = 255] inbox_url -> Varchar, @@ -1044,20 +1036,6 @@ diesel::table! { } } -diesel::table! { - site_aggregates (site_id) { - site_id -> Int4, - users -> Int8, - posts -> Int8, - comments -> Int8, - communities -> Int8, - users_active_day -> Int8, - users_active_week -> Int8, - users_active_month -> Int8, - users_active_half_year -> Int8, - } -} - diesel::table! { site_language (site_id, language_id) { site_id -> Int4, @@ -1101,13 +1079,11 @@ 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_reply -> comment (comment_id)); diesel::joinable!(comment_reply -> person (recipient_id)); diesel::joinable!(comment_report -> comment (comment_id)); diesel::joinable!(community -> instance (instance_id)); diesel::joinable!(community_actions -> community (community_id)); -diesel::joinable!(community_aggregates -> community (community_id)); diesel::joinable!(community_language -> community (community_id)); diesel::joinable!(community_language -> language (language_id)); diesel::joinable!(community_report -> community (community_id)); @@ -1128,7 +1104,6 @@ diesel::joinable!(local_site_rate_limit -> local_site (local_site_id)); diesel::joinable!(local_user -> person (person_id)); diesel::joinable!(local_user_language -> language (language_id)); diesel::joinable!(local_user_language -> local_user (local_user_id)); -diesel::joinable!(local_user_vote_display_mode -> local_user (local_user_id)); diesel::joinable!(login_token -> local_user (user_id)); diesel::joinable!(mod_add_community -> community (community_id)); diesel::joinable!(mod_ban_from_community -> community (community_id)); @@ -1166,7 +1141,6 @@ diesel::joinable!(oauth_account -> local_user (local_user_id)); diesel::joinable!(oauth_account -> oauth_provider (oauth_provider_id)); diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(person -> instance (instance_id)); -diesel::joinable!(person_aggregates -> person (person_id)); diesel::joinable!(person_ban -> person (person_id)); diesel::joinable!(person_comment_mention -> comment (comment_id)); diesel::joinable!(person_comment_mention -> person (recipient_id)); @@ -1178,14 +1152,11 @@ diesel::joinable!(person_saved_combined -> comment (comment_id)); diesel::joinable!(person_saved_combined -> person (person_id)); diesel::joinable!(person_saved_combined -> post (post_id)); diesel::joinable!(post -> community (community_id)); +diesel::joinable!(post -> instance (instance_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_report -> post (post_id)); diesel::joinable!(post_tag -> post (post_id)); diesel::joinable!(post_tag -> tag (tag_id)); @@ -1196,8 +1167,11 @@ diesel::joinable!(report_combined -> comment_report (comment_report_id)); diesel::joinable!(report_combined -> community_report (community_report_id)); diesel::joinable!(report_combined -> post_report (post_report_id)); diesel::joinable!(report_combined -> private_message_report (private_message_report_id)); +diesel::joinable!(search_combined -> comment (comment_id)); +diesel::joinable!(search_combined -> community (community_id)); +diesel::joinable!(search_combined -> person (person_id)); +diesel::joinable!(search_combined -> post (post_id)); diesel::joinable!(site -> instance (instance_id)); -diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); diesel::joinable!(site_language -> site (site_id)); diesel::joinable!(tag -> community (community_id)); @@ -1212,12 +1186,10 @@ diesel::allow_tables_to_appear_in_same_query!( captcha_answer, comment, comment_actions, - comment_aggregates, comment_reply, comment_report, community, community_actions, - community_aggregates, community_language, community_report, custom_emoji, @@ -1237,7 +1209,6 @@ diesel::allow_tables_to_appear_in_same_query!( local_site_url_blocklist, local_user, local_user_language, - local_user_vote_display_mode, login_token, mod_add, mod_add_community, @@ -1256,7 +1227,6 @@ diesel::allow_tables_to_appear_in_same_query!( password_reset_request, person, person_actions, - person_aggregates, person_ban, person_comment_mention, person_content_combined, @@ -1264,7 +1234,6 @@ diesel::allow_tables_to_appear_in_same_query!( person_saved_combined, post, post_actions, - post_aggregates, post_report, post_tag, previously_run_sql, @@ -1274,10 +1243,10 @@ diesel::allow_tables_to_appear_in_same_query!( registration_application, remote_image, report_combined, + search_combined, secret, sent_activity, site, - site_aggregates, site_language, tag, tagline, diff --git a/crates/db_schema/src/schema_setup.rs b/crates/db_schema/src/schema_setup.rs index c6fc2f9b3..02c869dbd 100644 --- a/crates/db_schema/src/schema_setup.rs +++ b/crates/db_schema/src/schema_setup.rs @@ -20,6 +20,7 @@ use diesel::{ use diesel_migrations::MigrationHarness; use lemmy_utils::{error::LemmyResult, settings::SETTINGS}; use std::time::Instant; +use tracing::debug; diesel::table! { pg_namespace (nspname) { @@ -49,8 +50,7 @@ const REPLACEABLE_SCHEMA_PATH: &str = "crates/db_schema/replaceable_schema"; struct MigrationHarnessWrapper<'a> { conn: &'a mut PgConnection, - #[cfg(test)] - enable_diff_check: bool, + options: &'a Options, } impl MigrationHarnessWrapper<'_> { @@ -66,7 +66,7 @@ impl MigrationHarnessWrapper<'_> { .map(|d| d.to_string()) .unwrap_or_default(); let name = migration.name(); - println!("{duration} run {name}"); + self.options.print(&format!("{duration} run {name}")); result } @@ -78,7 +78,7 @@ impl MigrationHarness for MigrationHarnessWrapper<'_> { migration: &dyn Migration, ) -> diesel::migration::Result> { #[cfg(test)] - if self.enable_diff_check { + if self.options.enable_diff_check { let before = diff_check::get_dump(); self.run_migration_inner(migration)?; @@ -111,7 +111,7 @@ impl MigrationHarness for MigrationHarnessWrapper<'_> { .map(|d| d.to_string()) .unwrap_or_default(); let name = migration.name(); - println!("{duration} revert {name}"); + self.options.print(&format!("{duration} revert {name}")); result } @@ -127,6 +127,7 @@ pub struct Options { enable_diff_check: bool, revert: bool, run: bool, + print_output: bool, limit: Option, } @@ -151,6 +152,21 @@ impl Options { self.limit = Some(limit); self } + + /// If print_output is true, use println!. + /// Otherwise, use debug! + pub fn print_output(mut self) -> Self { + self.print_output = true; + self + } + + fn print(&self, text: &str) { + if self.print_output { + println!("{text}"); + } else { + debug!("{text}"); + } + } } /// Checked by tests @@ -191,9 +207,9 @@ pub fn run(options: Options) -> LemmyResult { // Block concurrent attempts to run migrations until `conn` is closed, and disable the // trigger that prevents the Diesel CLI from running migrations - println!("Waiting for lock..."); + options.print("Waiting for lock..."); conn.batch_execute("SELECT pg_advisory_lock(0);")?; - println!("Running Database migrations (This may take a long time)..."); + options.print("Running Database migrations (This may take a long time)..."); // Drop `r` schema, so migrations don't need to be made to work both with and without things in // it existing @@ -226,7 +242,7 @@ pub fn run(options: Options) -> LemmyResult { Branch::ReplaceableSchemaNotRebuilt }; - println!("Database migrations complete."); + options.print("Database migrations complete."); Ok(output) } @@ -264,8 +280,7 @@ fn run_selected_migrations( ) -> diesel::migration::Result<()> { let mut wrapper = MigrationHarnessWrapper { conn, - #[cfg(test)] - enable_diff_check: options.enable_diff_check, + options, }; if options.revert { diff --git a/crates/db_schema/src/source/combined/mod.rs b/crates/db_schema/src/source/combined/mod.rs index 2555ef5be..458fdd519 100644 --- a/crates/db_schema/src/source/combined/mod.rs +++ b/crates/db_schema/src/source/combined/mod.rs @@ -3,3 +3,4 @@ pub mod modlog; pub mod person_content; pub mod person_saved; pub mod report; +pub mod search; diff --git a/crates/db_schema/src/source/combined/report.rs b/crates/db_schema/src/source/combined/report.rs index 2902c5548..d8a927285 100644 --- a/crates/db_schema/src/source/combined/report.rs +++ b/crates/db_schema/src/source/combined/report.rs @@ -1,4 +1,10 @@ -use crate::newtypes::{CommentReportId, PostReportId, PrivateMessageReportId, ReportCombinedId}; +use crate::newtypes::{ + CommentReportId, + CommunityReportId, + PostReportId, + PrivateMessageReportId, + ReportCombinedId, +}; #[cfg(feature = "full")] use crate::schema::report_combined; use chrono::{DateTime, Utc}; @@ -20,4 +26,5 @@ pub struct ReportCombined { pub post_report_id: Option, pub comment_report_id: Option, pub private_message_report_id: Option, + pub community_report_id: Option, } diff --git a/crates/db_schema/src/source/combined/search.rs b/crates/db_schema/src/source/combined/search.rs new file mode 100644 index 000000000..fe7387fea --- /dev/null +++ b/crates/db_schema/src/source/combined/search.rs @@ -0,0 +1,28 @@ +use crate::newtypes::{CommentId, CommunityId, PersonId, PostId, SearchCombinedId}; +#[cfg(feature = "full")] +use crate::schema::search_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = search_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = search_combined_keys))] +/// A combined table for a search (posts, comments, communities, persons) +pub struct SearchCombined { + pub id: SearchCombinedId, + pub published: DateTime, + pub score: i64, + pub post_id: Option, + pub comment_id: Option, + pub community_id: Option, + pub person_id: Option, +} diff --git a/crates/db_schema/src/source/comment.rs b/crates/db_schema/src/source/comment.rs index cc5d8c20c..a66dad02c 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -14,7 +14,7 @@ use serde_with::skip_serializing_none; use ts_rs::TS; #[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[cfg_attr( feature = "full", derive(Queryable, Selectable, Associations, Identifiable, TS) @@ -51,6 +51,17 @@ pub struct Comment { /// Whether the comment has been distinguished(speaking officially) by a mod. pub distinguished: bool, pub language_id: LanguageId, + pub score: i64, + pub upvotes: i64, + pub downvotes: i64, + /// The total number of children in this comment branch. + pub child_count: i32, + #[serde(skip)] + pub hot_rank: f64, + #[serde(skip)] + pub controversy_rank: f64, + pub report_count: i16, + pub unresolved_report_count: i16, } #[derive(Debug, Clone, derive_new::new)] diff --git a/crates/db_schema/src/source/comment_report.rs b/crates/db_schema/src/source/comment_report.rs index a19b6925a..6a7f0de97 100644 --- a/crates/db_schema/src/source/comment_report.rs +++ b/crates/db_schema/src/source/comment_report.rs @@ -30,6 +30,7 @@ pub struct CommentReport { pub published: DateTime, #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, + pub violates_instance_rules: bool, } #[derive(Clone)] @@ -40,4 +41,5 @@ pub struct CommentReportForm { pub comment_id: CommentId, pub original_comment_text: String, pub reason: String, + pub violates_instance_rules: bool, } diff --git a/crates/db_schema/src/source/community.rs b/crates/db_schema/src/source/community.rs index 92dc3c16a..33f62edce 100644 --- a/crates/db_schema/src/source/community.rs +++ b/crates/db_schema/src/source/community.rs @@ -16,7 +16,7 @@ use strum::{Display, EnumString}; use ts_rs::TS; #[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = community))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] @@ -39,8 +39,8 @@ pub struct Community { pub deleted: bool, /// Whether its an NSFW community. pub nsfw: bool, - /// The federated actor_id. - pub actor_id: DbUrl, + /// The federated ap_id. + pub ap_id: DbUrl, /// Whether the community is local. pub local: bool, #[serde(skip)] @@ -78,6 +78,25 @@ pub struct Community { pub description: Option, #[serde(skip)] pub random_number: i16, + pub subscribers: i64, + pub posts: i64, + pub comments: i64, + /// The number of users with any activity in the last day. + pub users_active_day: i64, + /// The number of users with any activity in the last week. + pub users_active_week: i64, + /// The number of users with any activity in the last month. + pub users_active_month: i64, + /// The number of users with any activity in the last year. + pub users_active_half_year: i64, + #[serde(skip)] + pub hot_rank: f64, + pub subscribers_local: i64, + pub report_count: i16, + pub unresolved_report_count: i16, + /// Number of any interactions over the last month. + #[serde(skip)] + pub interactions_month: i64, } #[derive(Debug, Clone, derive_new::new)] @@ -101,7 +120,7 @@ pub struct CommunityInsertForm { #[new(default)] pub nsfw: Option, #[new(default)] - pub actor_id: Option, + pub ap_id: Option, #[new(default)] pub local: Option, #[new(default)] @@ -141,7 +160,7 @@ pub struct CommunityUpdateForm { pub updated: Option>>, pub deleted: Option, pub nsfw: Option, - pub actor_id: Option, + pub ap_id: Option, pub local: Option, pub public_key: Option, pub private_key: Option>, diff --git a/crates/db_schema/src/source/images.rs b/crates/db_schema/src/source/images.rs index 34d1bb43b..14ce758ce 100644 --- a/crates/db_schema/src/source/images.rs +++ b/crates/db_schema/src/source/images.rs @@ -62,14 +62,17 @@ pub struct ImageDetails { pub width: i32, pub height: i32, pub content_type: String, + #[cfg_attr(feature = "full", ts(optional))] + pub blurhash: Option, } #[derive(Debug, Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = image_details))] -pub struct ImageDetailsForm { +pub struct ImageDetailsInsertForm { pub link: DbUrl, pub width: i32, pub height: i32, pub content_type: String, + pub blurhash: Option, } diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index 25ec40ca9..0641c12f8 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -17,7 +17,7 @@ use ts_rs::TS; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)] -#[cfg_attr(feature = "full", derive(Queryable, Identifiable, TS))] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = local_site))] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::site::Site)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] @@ -86,6 +86,23 @@ pub struct LocalSite { /// If this is true, users will never see the dialog asking to support Lemmy development with /// donations. pub disable_donation_dialog: bool, + #[cfg_attr(feature = "full", ts(optional))] + /// A default time range limit to apply to post sorts, in seconds. + pub default_post_time_range_seconds: Option, + pub users: i64, + pub posts: i64, + pub comments: i64, + pub communities: i64, + /// The number of users with any activity in the last day. + pub users_active_day: i64, + /// The number of users with any activity in the last week. + pub users_active_week: i64, + /// The number of users with any activity in the last month. + pub users_active_month: i64, + /// The number of users with any activity in the last half year. + pub users_active_half_year: i64, + /// Block NSFW content being created + pub disallow_nsfw_content: bool, } #[derive(Clone, derive_new::new)] @@ -147,6 +164,10 @@ pub struct LocalSiteInsertForm { pub comment_downvotes: Option, #[new(default)] pub disable_donation_dialog: Option, + #[new(default)] + pub default_post_time_range_seconds: Option>, + #[new(default)] + pub disallow_nsfw_content: bool, } #[derive(Clone, Default)] @@ -181,4 +202,6 @@ pub struct LocalSiteUpdateForm { pub comment_upvotes: Option, pub comment_downvotes: Option, pub disable_donation_dialog: Option, + pub default_post_time_range_seconds: Option>, + pub disallow_nsfw_content: Option, } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 26e1c5475..a3adf2834 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -76,6 +76,13 @@ pub struct LocalUser { pub last_donation_notification: DateTime, /// Whether to hide posts containing images/videos pub hide_media: bool, + #[cfg_attr(feature = "full", ts(optional))] + /// A default time range limit to apply to post sorts, in seconds. + pub default_post_time_range_seconds: Option, + pub show_score: bool, + pub show_upvotes: bool, + pub show_downvotes: bool, + pub show_upvote_percentage: bool, } #[derive(Clone, derive_new::new)] @@ -138,6 +145,16 @@ pub struct LocalUserInsertForm { pub last_donation_notification: Option>, #[new(default)] pub hide_media: Option, + #[new(default)] + pub default_post_time_range_seconds: Option>, + #[new(default)] + pub show_score: Option, + #[new(default)] + pub show_upvotes: Option, + #[new(default)] + pub show_downvotes: Option, + #[new(default)] + pub show_upvote_percentage: Option, } #[derive(Clone, Default)] @@ -172,4 +189,9 @@ pub struct LocalUserUpdateForm { pub auto_mark_fetched_posts_as_read: Option, pub last_donation_notification: Option>, pub hide_media: Option, + pub default_post_time_range_seconds: Option>, + pub show_score: Option, + pub show_upvotes: Option, + pub show_downvotes: Option, + pub show_upvote_percentage: Option, } diff --git a/crates/db_schema/src/source/local_user_language.rs b/crates/db_schema/src/source/local_user_language.rs deleted file mode 100644 index 83c666636..000000000 --- a/crates/db_schema/src/source/local_user_language.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::newtypes::{LanguageId, LocalUserId, LocalUserLanguageId}; -#[cfg(feature = "full")] -use crate::schema::local_user_language; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable))] -#[cfg_attr(feature = "full", diesel(table_name = local_user_language))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -pub struct LocalUserLanguage { - #[serde(skip)] - pub id: LocalUserLanguageId, - pub local_user_id: LocalUserId, - pub language_id: LanguageId, -} - -#[derive(Clone)] -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = local_user_language))] -pub struct LocalUserLanguageForm { - pub local_user_id: LocalUserId, - pub language_id: LanguageId, -} diff --git a/crates/db_schema/src/source/local_user_vote_display_mode.rs b/crates/db_schema/src/source/local_user_vote_display_mode.rs deleted file mode 100644 index 06a433034..000000000 --- a/crates/db_schema/src/source/local_user_vote_display_mode.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::newtypes::LocalUserId; -#[cfg(feature = "full")] -use crate::schema::local_user_vote_display_mode; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -#[cfg(feature = "full")] -use ts_rs::TS; - -#[skip_serializing_none] -#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = local_user_vote_display_mode))] -#[cfg_attr(feature = "full", diesel(primary_key(local_user_id)))] -#[cfg_attr( - feature = "full", - diesel(belongs_to(crate::source::local_site::LocalUser)) -)] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// The vote display settings for your user. -pub struct LocalUserVoteDisplayMode { - #[serde(skip)] - pub local_user_id: LocalUserId, - pub score: bool, - pub upvotes: bool, - pub downvotes: bool, - pub upvote_percentage: bool, -} - -#[derive(Clone, derive_new::new)] -#[cfg_attr(feature = "full", derive(Insertable))] -#[cfg_attr(feature = "full", diesel(table_name = local_user_vote_display_mode))] -pub struct LocalUserVoteDisplayModeInsertForm { - pub local_user_id: LocalUserId, - #[new(default)] - pub score: Option, - #[new(default)] - pub upvotes: Option, - #[new(default)] - pub downvotes: Option, - #[new(default)] - pub upvote_percentage: Option, -} - -#[derive(Clone, Default)] -#[cfg_attr(feature = "full", derive(AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = local_user_vote_display_mode))] -pub struct LocalUserVoteDisplayModeUpdateForm { - pub score: Option, - pub upvotes: Option, - pub downvotes: Option, - pub upvote_percentage: Option, -} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index c34be3726..a9a99c433 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -26,7 +26,6 @@ pub mod local_site; pub mod local_site_rate_limit; pub mod local_site_url_blocklist; pub mod local_user; -pub mod local_user_vote_display_mode; pub mod login_token; pub mod mod_log; pub mod oauth_account; @@ -37,6 +36,7 @@ pub mod person_block; pub mod person_comment_mention; pub mod person_post_mention; pub mod post; +pub mod post_actions; pub mod post_report; pub mod private_message; pub mod private_message_report; diff --git a/crates/db_schema/src/source/person.rs b/crates/db_schema/src/source/person.rs index 9c2a2d426..db78421ef 100644 --- a/crates/db_schema/src/source/person.rs +++ b/crates/db_schema/src/source/person.rs @@ -34,8 +34,8 @@ pub struct Person { pub published: DateTime, #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, - /// The federated actor_id. - pub actor_id: DbUrl, + /// The federated ap_id. + pub ap_id: DbUrl, /// An optional bio, in markdown. #[cfg_attr(feature = "full", ts(optional))] pub bio: Option, @@ -64,6 +64,12 @@ pub struct Person { #[cfg_attr(feature = "full", ts(optional))] pub ban_expires: Option>, pub instance_id: InstanceId, + pub post_count: i64, + #[serde(skip)] + pub post_score: i64, + pub comment_count: i64, + #[serde(skip)] + pub comment_score: i64, } #[derive(Clone, derive_new::new)] @@ -84,7 +90,7 @@ pub struct PersonInsertForm { #[new(default)] pub updated: Option>, #[new(default)] - pub actor_id: Option, + pub ap_id: Option, #[new(default)] pub bio: Option, #[new(default)] @@ -115,7 +121,7 @@ pub struct PersonUpdateForm { pub avatar: Option>, pub banned: Option, pub updated: Option>>, - pub actor_id: Option, + pub ap_id: Option, pub bio: Option>, pub local: Option, pub public_key: Option, diff --git a/crates/db_schema/src/source/post.rs b/crates/db_schema/src/source/post.rs index 8280fe8fe..f466378d4 100644 --- a/crates/db_schema/src/source/post.rs +++ b/crates/db_schema/src/source/post.rs @@ -1,22 +1,25 @@ -use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId}; -#[cfg(feature = "full")] -use crate::schema::{post, post_actions}; +use crate::newtypes::{CommunityId, DbUrl, InstanceId, LanguageId, PersonId, PostId}; 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}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] -use ts_rs::TS; +use { + crate::schema::{post, post_actions}, + diesel::{dsl, expression_methods::NullableExpressionMethods}, + i_love_jesus::CursorKeysModule, + ts_rs::TS, +}; #[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[cfg_attr( + feature = "full", + derive(Queryable, Selectable, Identifiable, TS, CursorKeysModule) +)] #[cfg_attr(feature = "full", diesel(table_name = post))] #[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = post_keys))] /// A post. pub struct Post { pub id: PostId, @@ -69,6 +72,28 @@ pub struct Post { /// Time at which the post will be published. None means publish immediately. #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option>, + pub comments: i64, + pub score: i64, + pub upvotes: i64, + pub downvotes: i64, + #[serde(skip)] + /// A newest comment time, limited to 2 days, to prevent necrobumping + pub newest_comment_time_necro: DateTime, + /// The time of the newest comment in the post. + pub newest_comment_time: DateTime, + #[serde(skip)] + pub hot_rank: f64, + #[serde(skip)] + pub hot_rank_active: f64, + #[serde(skip)] + pub controversy_rank: f64, + #[serde(skip)] + pub instance_id: InstanceId, + /// A rank that amplifies smaller communities + #[serde(skip)] + pub scaled_rank: f64, + pub report_count: i16, + pub unresolved_report_count: i16, } #[derive(Debug, Clone, derive_new::new)] diff --git a/crates/db_schema/src/source/post_actions.rs b/crates/db_schema/src/source/post_actions.rs new file mode 100644 index 000000000..3c4378951 --- /dev/null +++ b/crates/db_schema/src/source/post_actions.rs @@ -0,0 +1,42 @@ +use crate::newtypes::{PersonId, PostId}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use { + crate::schema::post_actions, + diesel::{dsl, expression_methods::NullableExpressionMethods}, +}; + +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr( + feature = "full", + derive(Queryable, Selectable, Associations, Identifiable) +)] +#[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)))] +/// Aggregate data for a person's post. +pub struct PostActions { + pub person_id: PersonId, + pub post_id: PostId, + /// 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 = post_actions))] +pub struct PostActionsForm { + pub person_id: PersonId, + pub post_id: PostId, + #[cfg_attr(feature = "full", diesel(column_name = read_comments_amount))] + pub read_comments: i64, +} diff --git a/crates/db_schema/src/source/post_report.rs b/crates/db_schema/src/source/post_report.rs index 610e495ae..5847d5c66 100644 --- a/crates/db_schema/src/source/post_report.rs +++ b/crates/db_schema/src/source/post_report.rs @@ -37,6 +37,7 @@ pub struct PostReport { pub published: DateTime, #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, + pub violates_instance_rules: bool, } #[derive(Clone, Default)] @@ -49,4 +50,5 @@ pub struct PostReportForm { pub original_post_url: Option, pub original_post_body: Option, pub reason: String, + pub violates_instance_rules: bool, } diff --git a/crates/db_schema/src/source/private_message.rs b/crates/db_schema/src/source/private_message.rs index f15373907..68db9154a 100644 --- a/crates/db_schema/src/source/private_message.rs +++ b/crates/db_schema/src/source/private_message.rs @@ -33,6 +33,7 @@ pub struct PrivateMessage { pub updated: Option>, pub ap_id: DbUrl, pub local: bool, + pub removed: bool, } #[derive(Clone, derive_new::new)] @@ -67,4 +68,5 @@ pub struct PrivateMessageUpdateForm { pub updated: Option>>, pub ap_id: Option, pub local: Option, + pub removed: Option, } diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs index 0fe33de01..e438e74b4 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -35,8 +35,8 @@ pub struct Site { /// A shorter, one-line description of the site. #[cfg_attr(feature = "full", ts(optional))] pub description: Option, - /// The federated actor_id. - pub actor_id: DbUrl, + /// The federated ap_id. + pub ap_id: DbUrl, /// The time the site was last refreshed. pub last_refreshed_at: DateTime, /// The site inbox @@ -69,7 +69,7 @@ pub struct SiteInsertForm { #[new(default)] pub description: Option, #[new(default)] - pub actor_id: Option, + pub ap_id: Option, #[new(default)] pub last_refreshed_at: Option>, #[new(default)] @@ -94,7 +94,7 @@ pub struct SiteUpdateForm { pub icon: Option>, pub banner: Option>, pub description: Option>, - pub actor_id: Option, + pub ap_id: Option, pub last_refreshed_at: Option>, pub inbox_url: Option, pub private_key: Option>, diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index bc30c6fb9..7ad8318fb 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -1,5 +1,5 @@ use crate::{ - newtypes::{CommunityId, DbUrl, PersonId}, + newtypes::{CommunityId, DbUrl, PaginationCursor, PersonId}, utils::{get_conn, uplete, DbPool}, }; use diesel::{ @@ -15,6 +15,8 @@ use diesel_async::{ AsyncPgConnection, RunQueryDsl, }; +use lemmy_utils::error::LemmyResult; +use std::future::Future; /// Returned by `diesel::delete` pub type Delete = DeleteStatement<::Table, ::WhereClause>; @@ -26,7 +28,6 @@ pub type PrimaryKey = <::Table as Table>::PrimaryKey; // Trying to create default implementations for `create` and `update` results in a lifetime mess and // weird compile errors. https://github.com/rust-lang/rust/issues/102211 -#[async_trait] pub trait Crud: HasTable + Sized where Self::Table: FindDsl, @@ -40,160 +41,234 @@ where type UpdateForm; type IdType: Send; - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result; + fn create( + pool: &mut DbPool<'_>, + form: &Self::InsertForm, + ) -> impl Future> + Send; - async fn read(pool: &mut DbPool<'_>, id: Self::IdType) -> Result { - let query: Find = Self::table().find(id); - let conn = &mut *get_conn(pool).await?; - query.first(conn).await + fn read( + pool: &mut DbPool<'_>, + id: Self::IdType, + ) -> impl Future> + Send + where + Self: Send, + { + async { + let query: Find = Self::table().find(id); + let conn = &mut *get_conn(pool).await?; + query.first(conn).await + } } /// when you want to null out a column, you have to send Some(None)), since sending None means you /// just don't want to update that column. - async fn update( + fn update( pool: &mut DbPool<'_>, id: Self::IdType, form: &Self::UpdateForm, - ) -> Result; + ) -> impl Future> + Send; - async fn delete(pool: &mut DbPool<'_>, id: Self::IdType) -> Result { - let query: Delete> = diesel::delete(Self::table().find(id)); - let conn = &mut *get_conn(pool).await?; - query.execute(conn).await + fn delete( + pool: &mut DbPool<'_>, + id: Self::IdType, + ) -> impl Future> + Send { + async { + let query: Delete> = diesel::delete(Self::table().find(id)); + let conn = &mut *get_conn(pool).await?; + query.execute(conn).await + } } } -#[async_trait] pub trait Followable { type Form; - async fn follow(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn follow( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; - async fn follow_accepted( + fn follow_accepted( pool: &mut DbPool<'_>, community_id: CommunityId, person_id: PersonId, - ) -> Result + ) -> impl Future> + Send where Self: Sized; - async fn unfollow(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn unfollow( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; } -#[async_trait] pub trait Joinable { type Form; - async fn join(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn join( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; - async fn leave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn leave( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; } -#[async_trait] pub trait Likeable { type Form; type IdType; - async fn like(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn like( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; - async fn remove( + fn remove( pool: &mut DbPool<'_>, person_id: PersonId, item_id: Self::IdType, - ) -> Result + ) -> impl Future> + Send where Self: Sized; } -#[async_trait] pub trait Bannable { type Form; - async fn ban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn ban( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; - async fn unban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn unban( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; } -#[async_trait] pub trait Saveable { type Form; - async fn save(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn save( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; - async fn unsave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn unsave( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; } -#[async_trait] pub trait Blockable { type Form; - async fn block(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn block( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; - async fn unblock(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn unblock( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; } -#[async_trait] pub trait Reportable { type Form; type IdType; type ObjectIdType; - async fn report(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + fn report( + pool: &mut DbPool<'_>, + form: &Self::Form, + ) -> impl Future> + Send where Self: Sized; - async fn resolve( + fn resolve( pool: &mut DbPool<'_>, report_id: Self::IdType, resolver_id: PersonId, - ) -> Result + ) -> impl Future> + Send where Self: Sized; - async fn resolve_all_for_object( + fn resolve_apub( + pool: &mut DbPool<'_>, + object_id: Self::ObjectIdType, + report_creator_id: PersonId, + resolver_id: PersonId, + ) -> impl Future> + Send + where + Self: Sized; + fn resolve_all_for_object( pool: &mut DbPool<'_>, comment_id_: Self::ObjectIdType, by_resolver_id: PersonId, - ) -> Result + ) -> impl Future> + Send where Self: Sized; - async fn unresolve( + fn unresolve( pool: &mut DbPool<'_>, report_id: Self::IdType, resolver_id: PersonId, - ) -> Result + ) -> impl Future> + Send where Self: Sized; } -#[async_trait] pub trait ApubActor { - async fn read_from_apub_id( + fn read_from_apub_id( pool: &mut DbPool<'_>, object_id: &DbUrl, - ) -> Result, Error> + ) -> impl Future, Error>> + Send where Self: Sized; /// - actor_name is the name of the community or user to read. /// - include_deleted, if true, will return communities or users that were deleted/removed - async fn read_from_name( + fn read_from_name( pool: &mut DbPool<'_>, actor_name: &str, include_deleted: bool, - ) -> Result, Error> + ) -> impl Future, Error>> + Send where Self: Sized; - async fn read_from_name_and_domain( + fn read_from_name_and_domain( pool: &mut DbPool<'_>, actor_name: &str, protocol_domain: &str, - ) -> Result, Error> + ) -> impl Future, Error>> + Send + where + Self: Sized; +} + +pub trait InternalToCombinedView { + type CombinedView; + + /// Maps the combined DB row to an enum + fn map_to_enum(self) -> Option; +} + +pub trait PaginationCursorBuilder { + type CursorData; + + /// Builds a pagination cursor for the given query result. + fn to_cursor(&self) -> PaginationCursor; + + /// Reads a database row from a given pagination cursor. + fn from_cursor( + cursor: &PaginationCursor, + conn: &mut DbPool<'_>, + ) -> impl Future> + Send where Self: Sized; } diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 870c02fdd..d3f5a17d4 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,30 +1,23 @@ pub mod uplete; -use crate::{newtypes::DbUrl, schema_setup, CommentSortType, PostSortType}; +use crate::{newtypes::DbUrl, schema_setup}; use chrono::TimeDelta; use deadpool::Runtime; use diesel::{ dsl, expression::AsExpression, helper_types::AsExprOf, - pg::Pg, + pg::{data_types::PgInterval, Pg}, query_builder::{Query, QueryFragment}, - query_dsl::methods::{FilterDsl, FindDsl, LimitDsl}, - query_source::{Alias, AliasSource, AliasedField}, + query_dsl::methods::LimitDsl, result::{ ConnectionError, ConnectionResult, Error::{self as DieselError, QueryBuilderError}, }, - sql_types::{self, SingleValue, Timestamptz}, - Column, + sql_types::{self, Timestamptz}, Expression, - ExpressionMethods, IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QuerySource, - Table, }; use diesel_async::{ pg::AsyncPgConnection, @@ -35,8 +28,7 @@ use diesel_async::{ }, AsyncConnection, }; -use diesel_bind_if_some::BindIfSome; -use futures_util::{future::BoxFuture, Future, FutureExt}; +use futures_util::{future::BoxFuture, FutureExt}; use i_love_jesus::{CursorKey, PaginatedQueryBuilder}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, @@ -311,6 +303,16 @@ pub fn diesel_string_update(opt: Option<&str>) -> Option> { } } +/// Takes an API optional number, and converts it to an optional diesel DB update. Zero means erase. +pub fn diesel_opt_number_update(opt: Option) -> Option> { + match opt { + // Zero is an erase + Some(0) => Some(None), + Some(num) => Some(Some(num)), + None => None, + } +} + /// Takes an API optional text input, and converts it to an optional diesel DB update (for non /// nullable properties). pub fn diesel_required_string_update(opt: Option<&str>) -> Option { @@ -503,18 +505,6 @@ pub fn build_db_pool_for_tests() -> ActualDbPool { build_db_pool().expect("db pool missing") } -pub fn post_to_comment_sort_type(sort: PostSortType) -> CommentSortType { - use PostSortType::*; - match sort { - Active | Hot | Scaled => CommentSortType::Hot, - New | NewComments | MostComments => CommentSortType::New, - Old => CommentSortType::Old, - Controversial => CommentSortType::Controversial, - TopHour | TopSixHour | TopTwelveHour | TopDay | TopAll | TopWeek | TopYear | TopMonth - | TopThreeMonths | TopSixMonths | TopNineMonths => CommentSortType::Top, - } -} - #[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-]+)*$") @@ -531,7 +521,7 @@ pub mod functions { define_sql_function! { #[sql_name = "r.scaled_rank"] - fn scaled_rank(score: BigInt, time: Timestamptz, users_active_month: BigInt) -> Double; + fn scaled_rank(score: BigInt, time: Timestamptz, interactions_month: BigInt) -> Double; } define_sql_function! { @@ -565,6 +555,10 @@ pub fn now() -> AsExprOf { diesel::dsl::now.into_sql::() } +pub fn seconds_to_pg_interval(seconds: i32) -> PgInterval { + PgInterval::from_microseconds(i64::from(seconds) * 1_000_000) +} + /// Trait alias for a type that can be converted to an SQL tuple using `IntoSql::into_sql` pub trait AsRecord: Expression + AsExpression> where @@ -580,102 +574,6 @@ impl>> AsRecord for T /// 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> {} @@ -686,58 +584,6 @@ pub trait ListFn<'a, T, Args>: Fn(DbConn<'a>, Args) -> ResultFuture<'a, Vec> impl<'a, T, Args, F: Fn(DbConn<'a>, Args) -> ResultFuture<'a, Vec>> ListFn<'a, T, Args> for F {} -/// Allows read and list functions to capture a shared closure that has an inferred return type, -/// which is useful for join logic -pub struct Queries { - pub read_fn: RF, - pub list_fn: LF, -} - -// `()` is used to prevent type inference error -impl Queries<(), ()> { - pub fn new<'a, RFut, LFut, RT, LT, RA, LA, RF2, LF2>( - read_fn: RF2, - list_fn: LF2, - ) -> Queries, impl ListFn<'a, LT, LA>> - where - RFut: Future> + Sized + Send + 'a, - LFut: Future, DieselError>> + Sized + Send + 'a, - RF2: Fn(DbConn<'a>, RA) -> RFut, - LF2: Fn(DbConn<'a>, LA) -> LFut, - { - Queries { - read_fn: move |conn, args| read_fn(conn, args).boxed(), - list_fn: move |conn, args| list_fn(conn, args).boxed(), - } - } -} - -impl Queries { - pub async fn read<'a, T, Args>( - self, - pool: &'a mut DbPool<'_>, - args: Args, - ) -> Result - where - RF: ReadFn<'a, T, Args>, - { - let conn = get_conn(pool).await?; - (self.read_fn)(conn, args).await - } - - pub async fn list<'a, T, Args>( - self, - pool: &'a mut DbPool<'_>, - args: Args, - ) -> Result, DieselError> - where - LF: ListFn<'a, T, Args>, - { - let conn = get_conn(pool).await?; - (self.list_fn)(conn, args).await - } -} - pub fn paginate( query: Q, page_after: Option, diff --git a/crates/db_schema/src/utils/uplete.rs b/crates/db_schema/src/utils/uplete.rs index 8c5262b90..dddbfe8ea 100644 --- a/crates/db_schema/src/utils/uplete.rs +++ b/crates/db_schema/src/utils/uplete.rs @@ -121,6 +121,9 @@ 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"); + // This is checked by require_uplete triggers + out.push_sql("/**/"); + // Declare `update_keys` and `delete_keys` CTEs, which select primary keys for (prefix, subquery) in [ ("WITH update_keys", &self.update_subquery), @@ -357,7 +360,7 @@ mod tests { 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: []"#) + format!(r#"/**/WITH {with_queries} SELECT ({update_count}), ({delete_count}) -- binds: []"#) } #[test] diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index f7d4b1d7a..4d90558fd 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -41,11 +41,11 @@ ts-rs = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } i-love-jesus = { workspace = true, optional = true } chrono = { workspace = true } -derive-new.workspace = true +derive-new = { workspace = true } [dev-dependencies] serial_test = { workspace = true } tokio = { workspace = true } pretty_assertions = { workspace = true } url = { workspace = true } -test-context = "0.3.0" +test-context = "0.4.1" diff --git a/crates/db_views_actor/src/inbox_combined_view.rs b/crates/db_views/src/combined/inbox_combined_view.rs similarity index 80% rename from crates/db_views_actor/src/inbox_combined_view.rs rename to crates/db_views/src/combined/inbox_combined_view.rs index 5c0405742..2faa4d68c 100644 --- a/crates/db_views_actor/src/inbox_combined_view.rs +++ b/crates/db_views/src/combined/inbox_combined_view.rs @@ -1,6 +1,5 @@ use crate::structs::{ CommentReplyView, - InboxCombinedPaginationCursor, InboxCombinedView, InboxCombinedViewInternal, PersonCommentMentionView, @@ -20,12 +19,12 @@ use diesel::{ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ - aliases::{self, creator_community_actions}, - newtypes::PersonId, + aliases::{self, creator_community_actions, creator_local_user}, + impls::{community::community_follower_select_subscribed_type, local_user::local_user_can_mod}, + newtypes::{PaginationCursor, PersonId}, schema::{ comment, comment_actions, - comment_aggregates, comment_reply, community, community_actions, @@ -39,43 +38,23 @@ use lemmy_db_schema::{ person_post_mention, post, post_actions, - post_aggregates, + post_tag, private_message, + tag, }, - source::{ - combined::inbox::{inbox_combined_keys as key, InboxCombined}, - community::CommunityFollower, - }, - utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, + source::combined::inbox::{inbox_combined_keys as key, InboxCombined}, + traits::{InternalToCombinedView, PaginationCursorBuilder}, + utils::{functions::coalesce, get_conn, DbPool}, InboxDataType, - InternalToCombinedView, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl InboxCombinedViewInternal { - /// Gets the number of unread mentions - pub async fn get_unread_count( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - show_bot_accounts: bool, - ) -> Result { - use diesel::dsl::count; - let conn = &mut get_conn(pool).await?; - + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(my_person_id: PersonId) -> _ { let item_creator = person::id; let recipient_person = aliases::person1.field(person::id); - let unread_filter = comment_reply::read - .eq(false) - .or(person_comment_mention::read.eq(false)) - .or(person_post_mention::read.eq(false)) - // If its unread, I only want the messages to me - .or( - private_message::read - .eq(false) - .and(private_message::recipient_id.eq(my_person_id)), - ); - let item_creator_join = comment::creator_id .eq(item_creator) .or( @@ -85,11 +64,13 @@ impl InboxCombinedViewInternal { ) .or(private_message::creator_id.eq(item_creator)); - let recipient_join = comment_reply::recipient_id - .eq(recipient_person) - .or(person_comment_mention::recipient_id.eq(recipient_person)) - .or(person_post_mention::recipient_id.eq(recipient_person)) - .or(private_message::recipient_id.eq(recipient_person)); + let recipient_join = aliases::person1.on( + comment_reply::recipient_id + .eq(recipient_person) + .or(person_comment_mention::recipient_id.eq(recipient_person)) + .or(person_post_mention::recipient_id.eq(recipient_person)) + .or(private_message::recipient_id.eq(recipient_person)), + ); let comment_join = comment_reply::comment_id .eq(comment::id) @@ -108,29 +89,107 @@ impl InboxCombinedViewInternal { // This could be a simple join, but you need to check for deleted here let private_message_join = inbox_combined::private_message_id .eq(private_message::id.nullable()) - .and(not(private_message::deleted)); + .and(not(private_message::deleted)) + .and(not(private_message::removed)); - let mut query = inbox_combined::table + let community_join = post::community_id.eq(community::id); + + let local_user_join = local_user::table.on(local_user::person_id.nullable().eq(my_person_id)); + + let creator_local_user_join = creator_local_user.on( + item_creator + .eq(creator_local_user.field(local_user::person_id)) + .and(creator_local_user.field(local_user::admin).eq(true)), + ); + + let image_details_join = + image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())); + + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(item_creator), + ), + ); + + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(post::community_id) + .and(community_actions::person_id.eq(my_person_id)), + ); + + let instance_actions_join = instance_actions::table.on( + instance_actions::instance_id + .eq(person::instance_id) + .and(instance_actions::person_id.eq(my_person_id)), + ); + + let post_actions_join = post_actions::table.on( + post_actions::post_id + .eq(post::id) + .and(post_actions::person_id.eq(my_person_id)), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(item_creator) + .and(person_actions::person_id.eq(my_person_id)), + ); + + let comment_actions_join = comment_actions::table.on( + comment_actions::comment_id + .eq(comment::id) + .and(comment_actions::person_id.eq(my_person_id)), + ); + + inbox_combined::table .left_join(comment_reply::table) .left_join(person_comment_mention::table) .left_join(person_post_mention::table) .left_join(private_message::table.on(private_message_join)) .left_join(comment::table.on(comment_join)) .left_join(post::table.on(post_join)) - // The item creator + .left_join(community::table.on(community_join)) .inner_join(person::table.on(item_creator_join)) - // The recipient - .inner_join(aliases::person1.on(recipient_join)) - .left_join(actions( - instance_actions::table, - Some(my_person_id), - person::instance_id, - )) - .left_join(actions( - person_actions::table, - Some(my_person_id), - item_creator, - )) + .inner_join(recipient_join) + .left_join(image_details_join) + .left_join(creator_community_actions_join) + .left_join(local_user_join) + .left_join(creator_local_user_join) + .left_join(community_actions_join) + .left_join(instance_actions_join) + .left_join(post_actions_join) + .left_join(person_actions_join) + .left_join(comment_actions_join) + } + + /// Gets the number of unread mentions + pub async fn get_unread_count( + pool: &mut DbPool<'_>, + my_person_id: PersonId, + show_bot_accounts: bool, + ) -> Result { + use diesel::dsl::count; + let conn = &mut get_conn(pool).await?; + + let recipient_person = aliases::person1.field(person::id); + + let unread_filter = comment_reply::read + .eq(false) + .or(person_comment_mention::read.eq(false)) + .or(person_post_mention::read.eq(false)) + // If its unread, I only want the messages to me + .or( + private_message::read + .eq(false) + .and(private_message::recipient_id.eq(my_person_id)), + ); + + let mut query = Self::joins(my_person_id) // Filter for your user .filter(recipient_person.eq(my_person_id)) // Filter unreads @@ -138,6 +197,7 @@ impl InboxCombinedViewInternal { // Don't count replies from blocked users .filter(person_actions::blocked.is_null()) .filter(instance_actions::blocked.is_null()) + .select(count(inbox_combined::id)) .into_boxed(); // These filters need to be kept in sync with the filters in queries().list() @@ -145,55 +205,53 @@ impl InboxCombinedViewInternal { query = query.filter(not(person::bot_account)); } - query - .select(count(inbox_combined::id)) - .first::(conn) - .await + query.first::(conn).await } } -impl InboxCombinedPaginationCursor { - // get cursor for page that starts immediately after the given post - pub fn after_post(view: &InboxCombinedView) -> InboxCombinedPaginationCursor { - let (prefix, id) = match view { +impl PaginationCursorBuilder for InboxCombinedView { + type CursorData = InboxCombined; + + fn to_cursor(&self) -> PaginationCursor { + let (prefix, id) = match &self { InboxCombinedView::CommentReply(v) => ('R', v.comment_reply.id.0), InboxCombinedView::CommentMention(v) => ('C', v.person_comment_mention.id.0), InboxCombinedView::PostMention(v) => ('P', v.person_post_mention.id.0), InboxCombinedView::PrivateMessage(v) => ('M', v.private_message.id.0), }; - // hex encoding to prevent ossification - InboxCombinedPaginationCursor(format!("{prefix}{id:x}")) + PaginationCursor::new(prefix, id) } - pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { - let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); - let mut query = inbox_combined::table - .select(InboxCombined::as_select()) - .into_boxed(); - let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; - let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; - query = match prefix { - "R" => query.filter(inbox_combined::comment_reply_id.eq(id)), - "C" => query.filter(inbox_combined::person_comment_mention_id.eq(id)), - "P" => query.filter(inbox_combined::person_post_mention_id.eq(id)), - "M" => query.filter(inbox_combined::private_message_id.eq(id)), - _ => return Err(err_msg()), - }; - let token = query.first(&mut get_conn(pool).await?).await?; + async fn from_cursor( + cursor: &PaginationCursor, + pool: &mut DbPool<'_>, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + let (prefix, id) = cursor.prefix_and_id()?; - Ok(PaginationCursorData(token)) + let mut query = inbox_combined::table + .select(Self::CursorData::as_select()) + .into_boxed(); + + query = match prefix { + 'R' => query.filter(inbox_combined::comment_reply_id.eq(id)), + 'C' => query.filter(inbox_combined::person_comment_mention_id.eq(id)), + 'P' => query.filter(inbox_combined::person_post_mention_id.eq(id)), + 'M' => query.filter(inbox_combined::private_message_id.eq(id)), + _ => return Err(LemmyErrorType::CouldntParsePaginationToken.into()), + }; + let token = query.first(conn).await?; + + Ok(token) } } -#[derive(Clone)] -pub struct PaginationCursorData(InboxCombined); - #[derive(Default)] pub struct InboxCombinedQuery { pub type_: Option, pub unread_only: Option, pub show_bot_accounts: Option, - pub page_after: Option, + pub cursor_data: Option, pub page_back: Option, } @@ -208,118 +266,46 @@ impl InboxCombinedQuery { let item_creator = person::id; let recipient_person = aliases::person1.field(person::id); - let item_creator_join = comment::creator_id - .eq(item_creator) - .or( - inbox_combined::person_post_mention_id - .is_not_null() - .and(post::creator_id.eq(item_creator)), - ) - .or(private_message::creator_id.eq(item_creator)); - - let recipient_join = comment_reply::recipient_id - .eq(recipient_person) - .or(person_comment_mention::recipient_id.eq(recipient_person)) - .or(person_post_mention::recipient_id.eq(recipient_person)) - .or(private_message::recipient_id.eq(recipient_person)); - - let comment_join = comment_reply::comment_id - .eq(comment::id) - .or(person_comment_mention::comment_id.eq(comment::id)) - // Filter out the deleted / removed - .and(not(comment::deleted)) - .and(not(comment::removed)); - - let post_join = person_post_mention::post_id - .eq(post::id) - .or(comment::post_id.eq(post::id)) - // Filter out the deleted / removed - .and(not(post::deleted)) - .and(not(post::removed)); - - // This could be a simple join, but you need to check for deleted here - let private_message_join = inbox_combined::private_message_id - .eq(private_message::id.nullable()) - .and(not(private_message::deleted)); - - let community_join = post::community_id.eq(community::id); - - let mut query = inbox_combined::table - .left_join(comment_reply::table) - .left_join(person_comment_mention::table) - .left_join(person_post_mention::table) - .left_join(private_message::table.on(private_message_join)) - .left_join(comment::table.on(comment_join)) - .left_join(post::table.on(post_join)) - .left_join(community::table.on(community_join)) - // The item creator - .inner_join(person::table.on(item_creator_join)) - // The recipient - .inner_join(aliases::person1.on(recipient_join)) - .left_join(actions_alias( - creator_community_actions, - item_creator, - post::community_id, + let post_tags = post_tag::table + .inner_join(tag::table) + .select(diesel::dsl::sql::( + "json_agg(tag.*)", )) - .left_join( - local_user::table.on( - item_creator - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ) - .left_join(actions( - community_actions::table, - Some(my_person_id), - post::community_id, - )) - .left_join(actions( - instance_actions::table, - Some(my_person_id), - person::instance_id, - )) - .left_join(actions(post_actions::table, Some(my_person_id), post::id)) - .left_join(actions( - person_actions::table, - Some(my_person_id), - item_creator, - )) - .left_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) - .left_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) - .left_join(actions( - comment_actions::table, - Some(my_person_id), - comment::id, - )) - .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + .filter(post_tag::post_id.eq(post::id)) + .filter(tag::deleted.eq(false)) + .single_value(); + + let mut query = InboxCombinedViewInternal::joins(my_person_id) .select(( // Specific comment_reply::all_columns.nullable(), person_comment_mention::all_columns.nullable(), person_post_mention::all_columns.nullable(), - post_aggregates::all_columns.nullable(), coalesce( - post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), - post_aggregates::comments, + post::comments.nullable() - post_actions::read_comments_amount.nullable(), + post::comments, ) .nullable(), - post_actions::saved.nullable().is_not_null(), + post_actions::saved.nullable(), post_actions::read.nullable().is_not_null(), post_actions::hidden.nullable().is_not_null(), post_actions::like_score.nullable(), image_details::all_columns.nullable(), + post_tags, private_message::all_columns.nullable(), // Shared post::all_columns.nullable(), community::all_columns.nullable(), comment::all_columns.nullable(), - comment_aggregates::all_columns.nullable(), - comment_actions::saved.nullable().is_not_null(), + comment_actions::saved.nullable(), comment_actions::like_score.nullable(), - CommunityFollower::select_subscribed_type(), + community_follower_select_subscribed_type(), person::all_columns, aliases::person1.fields(person::all_columns), - local_user::admin.nullable().is_not_null(), + creator_local_user + .field(local_user::admin) + .nullable() + .is_not_null(), creator_community_actions .field(community_actions::became_moderator) .nullable() @@ -330,6 +316,7 @@ impl InboxCombinedQuery { .is_not_null(), person_actions::blocked.nullable().is_not_null(), community_actions::received_ban.nullable().is_not_null(), + local_user_can_mod(), )) .into_boxed(); @@ -400,12 +387,10 @@ impl InboxCombinedQuery { let mut query = PaginatedQueryBuilder::new(query); - let page_after = self.page_after.map(|c| c.0); - if self.page_back.unwrap_or_default() { - query = query.before(page_after).limit_and_offset_from_end(); + query = query.before(self.cursor_data).limit_and_offset_from_end(); } else { - query = query.after(page_after); + query = query.after(self.cursor_data); } // Sorting by published @@ -433,17 +418,15 @@ impl InternalToCombinedView for InboxCombinedViewInternal { // Use for a short alias let v = self; - if let (Some(comment_reply), Some(comment), Some(counts), Some(post), Some(community)) = ( + if let (Some(comment_reply), Some(comment), Some(post), Some(community)) = ( v.comment_reply, v.comment.clone(), - v.comment_counts.clone(), v.post.clone(), v.community.clone(), ) { Some(InboxCombinedView::CommentReply(CommentReplyView { comment_reply, comment, - counts, recipient: v.item_recipient, post, community, @@ -456,17 +439,11 @@ impl InternalToCombinedView for InboxCombinedViewInternal { saved: v.comment_saved, my_vote: v.my_comment_vote, banned_from_community: v.banned_from_community, + can_mod: v.can_mod, })) - } else if let ( - Some(person_comment_mention), - Some(comment), - Some(counts), - Some(post), - Some(community), - ) = ( + } else if let (Some(person_comment_mention), Some(comment), Some(post), Some(community)) = ( v.person_comment_mention, v.comment, - v.comment_counts, v.post.clone(), v.community.clone(), ) { @@ -474,7 +451,6 @@ impl InternalToCombinedView for InboxCombinedViewInternal { PersonCommentMentionView { person_comment_mention, comment, - counts, recipient: v.item_recipient, post, community, @@ -487,24 +463,17 @@ impl InternalToCombinedView for InboxCombinedViewInternal { saved: v.comment_saved, my_vote: v.my_comment_vote, banned_from_community: v.banned_from_community, + can_mod: v.can_mod, }, )) - } else if let ( - Some(person_post_mention), - Some(post), - Some(counts), - Some(unread_comments), - Some(community), - ) = ( + } else if let (Some(person_post_mention), Some(post), Some(unread_comments), Some(community)) = ( v.person_post_mention, v.post, - v.post_counts, v.post_unread_comments, v.community, ) { Some(InboxCombinedView::PostMention(PersonPostMentionView { person_post_mention, - counts, post, community, recipient: v.item_recipient, @@ -520,7 +489,9 @@ impl InternalToCombinedView for InboxCombinedViewInternal { hidden: v.post_hidden, my_vote: v.my_post_vote, image_details: v.image_details, + post_tags: v.post_tags, banned_from_community: v.banned_from_community, + can_mod: v.can_mod, })) } else if let Some(private_message) = v.private_message { Some(InboxCombinedView::PrivateMessage(PrivateMessageView { @@ -538,7 +509,7 @@ impl InternalToCombinedView for InboxCombinedViewInternal { #[expect(clippy::indexing_slicing)] mod tests { use crate::{ - inbox_combined_view::InboxCombinedQuery, + combined::inbox_combined_view::InboxCombinedQuery, structs::{InboxCombinedView, InboxCombinedViewInternal, PrivateMessageView}, }; use lemmy_db_schema::{ diff --git a/crates/db_views/src/combined/mod.rs b/crates/db_views/src/combined/mod.rs new file mode 100644 index 000000000..d6f4576ee --- /dev/null +++ b/crates/db_views/src/combined/mod.rs @@ -0,0 +1,12 @@ +#[cfg(feature = "full")] +pub mod inbox_combined_view; +#[cfg(feature = "full")] +pub mod modlog_combined_view; +#[cfg(feature = "full")] +pub mod person_content_combined_view; +#[cfg(feature = "full")] +pub mod person_saved_combined_view; +#[cfg(feature = "full")] +pub mod report_combined_view; +#[cfg(feature = "full")] +pub mod search_combined_view; diff --git a/crates/db_views_moderator/src/modlog_combined_view.rs b/crates/db_views/src/combined/modlog_combined_view.rs similarity index 77% rename from crates/db_views_moderator/src/modlog_combined_view.rs rename to crates/db_views/src/combined/modlog_combined_view.rs index 4c6e619ea..c7a3dca82 100644 --- a/crates/db_views_moderator/src/modlog_combined_view.rs +++ b/crates/db_views/src/combined/modlog_combined_view.rs @@ -16,12 +16,10 @@ use crate::structs::{ ModRemoveCommunityView, ModRemovePostView, ModTransferCommunityView, - ModlogCombinedPaginationCursor, ModlogCombinedView, ModlogCombinedViewInternal, }; use diesel::{ - result::Error, BoolExpressionMethods, ExpressionMethods, IntoSql, @@ -34,7 +32,8 @@ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ aliases, - newtypes::{CommentId, CommunityId, PersonId, PostId}, + impls::local_user::LocalUserOptionHelper, + newtypes::{CommentId, CommunityId, PaginationCursor, PersonId, PostId}, schema::{ admin_allow_instance, admin_block_instance, @@ -44,6 +43,7 @@ use lemmy_db_schema::{ admin_purge_post, comment, community, + community_actions, instance, mod_add, mod_add_community, @@ -60,211 +60,146 @@ use lemmy_db_schema::{ person, post, }, - source::combined::modlog::{modlog_combined_keys as key, ModlogCombined}, + source::{ + combined::modlog::{modlog_combined_keys as key, ModlogCombined}, + local_user::LocalUser, + }, + traits::{InternalToCombinedView, PaginationCursorBuilder}, utils::{get_conn, DbPool}, - InternalToCombinedView, + ListingType, ModlogActionType, }; -use lemmy_utils::error::LemmyResult; - -impl ModlogCombinedPaginationCursor { - // get cursor for page that starts immediately after the given post - pub fn after_post(view: &ModlogCombinedView) -> ModlogCombinedPaginationCursor { - let (prefix, id) = match view { - ModlogCombinedView::AdminAllowInstance(v) => { - ("AdminAllowInstance", v.admin_allow_instance.id.0) - } - ModlogCombinedView::AdminBlockInstance(v) => { - ("AdminBlockInstance", v.admin_block_instance.id.0) - } - ModlogCombinedView::AdminPurgeComment(v) => ("AdminPurgeComment", v.admin_purge_comment.id.0), - ModlogCombinedView::AdminPurgeCommunity(v) => { - ("AdminPurgeCommunity", v.admin_purge_community.id.0) - } - ModlogCombinedView::AdminPurgePerson(v) => ("AdminPurgePerson", v.admin_purge_person.id.0), - ModlogCombinedView::AdminPurgePost(v) => ("AdminPurgePost", v.admin_purge_post.id.0), - ModlogCombinedView::ModAdd(v) => ("ModAdd", v.mod_add.id.0), - ModlogCombinedView::ModAddCommunity(v) => ("ModAddCommunity", v.mod_add_community.id.0), - ModlogCombinedView::ModBan(v) => ("ModBan", v.mod_ban.id.0), - ModlogCombinedView::ModBanFromCommunity(v) => { - ("ModBanFromCommunity", v.mod_ban_from_community.id.0) - } - ModlogCombinedView::ModFeaturePost(v) => ("ModFeaturePost", v.mod_feature_post.id.0), - ModlogCombinedView::ModHideCommunity(v) => ("ModHideCommunity", v.mod_hide_community.id.0), - ModlogCombinedView::ModLockPost(v) => ("ModLockPost", v.mod_lock_post.id.0), - ModlogCombinedView::ModRemoveComment(v) => ("ModRemoveComment", v.mod_remove_comment.id.0), - ModlogCombinedView::ModRemoveCommunity(v) => { - ("ModRemoveCommunity", v.mod_remove_community.id.0) - } - ModlogCombinedView::ModRemovePost(v) => ("ModRemovePost", v.mod_remove_post.id.0), - ModlogCombinedView::ModTransferCommunity(v) => { - ("ModTransferCommunity", v.mod_transfer_community.id.0) - } - }; - // hex encoding to prevent ossification - ModlogCombinedPaginationCursor(format!("{prefix}-{id:x}")) - } - - pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { - let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); - let mut query = modlog_combined::table - .select(ModlogCombined::as_select()) - .into_boxed(); - let (prefix, id_str) = self.0.split_once('-').ok_or_else(err_msg)?; - let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; - query = match prefix { - "AdminAllowInstance" => query.filter(modlog_combined::admin_allow_instance_id.eq(id)), - "AdminBlockInstance" => query.filter(modlog_combined::admin_block_instance_id.eq(id)), - "AdminPurgeComment" => query.filter(modlog_combined::admin_purge_comment_id.eq(id)), - "AdminPurgeCommunity" => query.filter(modlog_combined::admin_purge_community_id.eq(id)), - "AdminPurgePerson" => query.filter(modlog_combined::admin_purge_person_id.eq(id)), - "AdminPurgePost" => query.filter(modlog_combined::admin_purge_post_id.eq(id)), - "ModAdd" => query.filter(modlog_combined::mod_add_id.eq(id)), - "ModAddCommunity" => query.filter(modlog_combined::mod_add_community_id.eq(id)), - "ModBan" => query.filter(modlog_combined::mod_ban_id.eq(id)), - "ModBanFromCommunity" => query.filter(modlog_combined::mod_ban_from_community_id.eq(id)), - "ModFeaturePost" => query.filter(modlog_combined::mod_feature_post_id.eq(id)), - "ModHideCommunity" => query.filter(modlog_combined::mod_hide_community_id.eq(id)), - "ModLockPost" => query.filter(modlog_combined::mod_lock_post_id.eq(id)), - "ModRemoveComment" => query.filter(modlog_combined::mod_remove_comment_id.eq(id)), - "ModRemoveCommunity" => query.filter(modlog_combined::mod_remove_community_id.eq(id)), - "ModRemovePost" => query.filter(modlog_combined::mod_remove_post_id.eq(id)), - "ModTransferCommunity" => query.filter(modlog_combined::mod_transfer_community_id.eq(id)), - - _ => return Err(err_msg()), - }; - let token = query.first(&mut get_conn(pool).await?).await?; - - Ok(PaginationCursorData(token)) - } -} - -#[derive(Clone)] -pub struct PaginationCursorData(ModlogCombined); - -#[derive(Default)] -/// Querying / filtering the modlog. -pub struct ModlogCombinedQuery { - pub type_: Option, - pub comment_id: Option, - pub post_id: Option, - pub community_id: Option, - pub hide_modlog_names: Option, - pub mod_person_id: Option, - pub other_person_id: Option, - pub page_after: Option, - pub page_back: Option, -} - -impl ModlogCombinedQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> LemmyResult> { - let conn = &mut get_conn(pool).await?; - - let mod_person = self.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !(self.hide_modlog_names.unwrap_or_default()); - let show_mod_names_expr = show_mod_names.as_sql::(); +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +impl ModlogCombinedViewInternal { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins( + mod_person_id: Option, + hide_modlog_names: Option, + my_person_id: Option, + ) -> _ { // The modded / other person let other_person = aliases::person1.field(person::id); + let show_mod_names: bool = !(hide_modlog_names.unwrap_or_default()); + let show_mod_names_expr = show_mod_names.into_sql::(); + // The query for the admin / mod person // It needs an OR condition to every mod table // After this you can use person::id to refer to the moderator - let moderator_names_join = show_mod_names_expr.or(person::id.eq(mod_person)).and( - admin_allow_instance::admin_person_id - .eq(person::id) - .or(admin_block_instance::admin_person_id.eq(person::id)) - .or(admin_purge_comment::admin_person_id.eq(person::id)) - .or(admin_purge_community::admin_person_id.eq(person::id)) - .or(admin_purge_person::admin_person_id.eq(person::id)) - .or(admin_purge_post::admin_person_id.eq(person::id)) - .or(mod_add::mod_person_id.eq(person::id)) - .or(mod_add_community::mod_person_id.eq(person::id)) - .or(mod_ban::mod_person_id.eq(person::id)) - .or(mod_ban_from_community::mod_person_id.eq(person::id)) - .or(mod_feature_post::mod_person_id.eq(person::id)) - .or(mod_hide_community::mod_person_id.eq(person::id)) - .or(mod_lock_post::mod_person_id.eq(person::id)) - .or(mod_remove_comment::mod_person_id.eq(person::id)) - .or(mod_remove_community::mod_person_id.eq(person::id)) - .or(mod_remove_post::mod_person_id.eq(person::id)) - .or(mod_transfer_community::mod_person_id.eq(person::id)), + let moderator_names_join = person::table.on( + show_mod_names_expr + .or(person::id.nullable().eq(mod_person_id)) + .and( + admin_allow_instance::admin_person_id + .eq(person::id) + .or(admin_block_instance::admin_person_id.eq(person::id)) + .or(admin_purge_comment::admin_person_id.eq(person::id)) + .or(admin_purge_community::admin_person_id.eq(person::id)) + .or(admin_purge_person::admin_person_id.eq(person::id)) + .or(admin_purge_post::admin_person_id.eq(person::id)) + .or(mod_add::mod_person_id.eq(person::id)) + .or(mod_add_community::mod_person_id.eq(person::id)) + .or(mod_ban::mod_person_id.eq(person::id)) + .or(mod_ban_from_community::mod_person_id.eq(person::id)) + .or(mod_feature_post::mod_person_id.eq(person::id)) + .or(mod_hide_community::mod_person_id.eq(person::id)) + .or(mod_lock_post::mod_person_id.eq(person::id)) + .or(mod_remove_comment::mod_person_id.eq(person::id)) + .or(mod_remove_community::mod_person_id.eq(person::id)) + .or(mod_remove_post::mod_person_id.eq(person::id)) + .or(mod_transfer_community::mod_person_id.eq(person::id)), + ), ); - let other_person_join = mod_add::other_person_id - .eq(other_person) - .or(mod_add_community::other_person_id.eq(other_person)) - .or(mod_ban::other_person_id.eq(other_person)) - .or(mod_ban_from_community::other_person_id.eq(other_person)) - // Some tables don't have the other_person_id directly, so you need to join - .or( - mod_feature_post::id - .is_not_null() - .and(post::creator_id.eq(other_person)), - ) - .or( - mod_lock_post::id - .is_not_null() - .and(post::creator_id.eq(other_person)), - ) - .or( - mod_remove_comment::id - .is_not_null() - .and(comment::creator_id.eq(other_person)), - ) - .or( - mod_remove_post::id - .is_not_null() - .and(post::creator_id.eq(other_person)), - ) - .or(mod_transfer_community::other_person_id.eq(other_person)); + let other_person_join = aliases::person1.on( + mod_add::other_person_id + .eq(other_person) + .or(mod_add_community::other_person_id.eq(other_person)) + .or(mod_ban::other_person_id.eq(other_person)) + .or(mod_ban_from_community::other_person_id.eq(other_person)) + // Some tables don't have the other_person_id directly, so you need to join + .or( + mod_feature_post::id + .is_not_null() + .and(post::creator_id.eq(other_person)), + ) + .or( + mod_lock_post::id + .is_not_null() + .and(post::creator_id.eq(other_person)), + ) + .or( + mod_remove_comment::id + .is_not_null() + .and(comment::creator_id.eq(other_person)), + ) + .or( + mod_remove_post::id + .is_not_null() + .and(post::creator_id.eq(other_person)), + ) + .or(mod_transfer_community::other_person_id.eq(other_person)), + ); - let comment_join = mod_remove_comment::comment_id.eq(comment::id); + let comment_join = comment::table.on(mod_remove_comment::comment_id.eq(comment::id)); - let post_join = admin_purge_comment::post_id - .eq(post::id) - .or(mod_feature_post::post_id.eq(post::id)) - .or(mod_lock_post::post_id.eq(post::id)) - .or( - mod_remove_comment::id - .is_not_null() - .and(comment::post_id.eq(post::id)), - ) - .or(mod_remove_post::post_id.eq(post::id)); + let post_join = post::table.on( + admin_purge_comment::post_id + .eq(post::id) + .or(mod_feature_post::post_id.eq(post::id)) + .or(mod_lock_post::post_id.eq(post::id)) + .or( + mod_remove_comment::id + .is_not_null() + .and(comment::post_id.eq(post::id)), + ) + .or(mod_remove_post::post_id.eq(post::id)), + ); - let community_join = admin_purge_post::community_id - .eq(community::id) - .or(mod_add_community::community_id.eq(community::id)) - .or(mod_ban_from_community::community_id.eq(community::id)) - .or( - mod_feature_post::id - .is_not_null() - .and(post::community_id.eq(community::id)), - ) - .or(mod_hide_community::community_id.eq(community::id)) - .or( - mod_lock_post::id - .is_not_null() - .and(post::community_id.eq(community::id)), - ) - .or( - mod_remove_comment::id - .is_not_null() - .and(post::community_id.eq(community::id)), - ) - .or(mod_remove_community::community_id.eq(community::id)) - .or( - mod_remove_post::id - .is_not_null() - .and(post::community_id.eq(community::id)), - ) - .or(mod_transfer_community::community_id.eq(community::id)); + let community_join = community::table.on( + admin_purge_post::community_id + .eq(community::id) + .or(mod_add_community::community_id.eq(community::id)) + .or(mod_ban_from_community::community_id.eq(community::id)) + .or( + mod_feature_post::id + .is_not_null() + .and(post::community_id.eq(community::id)), + ) + .or(mod_hide_community::community_id.eq(community::id)) + .or( + mod_lock_post::id + .is_not_null() + .and(post::community_id.eq(community::id)), + ) + .or( + mod_remove_comment::id + .is_not_null() + .and(post::community_id.eq(community::id)), + ) + .or(mod_remove_community::community_id.eq(community::id)) + .or( + mod_remove_post::id + .is_not_null() + .and(post::community_id.eq(community::id)), + ) + .or(mod_transfer_community::community_id.eq(community::id)), + ); - let instance_join = admin_allow_instance::instance_id - .eq(instance::id) - .or(admin_block_instance::instance_id.eq(instance::id)); + let instance_join = instance::table.on( + admin_allow_instance::instance_id + .eq(instance::id) + .or(admin_block_instance::instance_id.eq(instance::id)), + ); - let mut query = modlog_combined::table + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(community::id) + .and(community_actions::person_id.nullable().eq(my_person_id)), + ); + + modlog_combined::table .left_join(admin_allow_instance::table) .left_join(admin_block_instance::table) .left_join(admin_purge_comment::table) @@ -282,46 +217,106 @@ impl ModlogCombinedQuery { .left_join(mod_remove_community::table) .left_join(mod_remove_post::table) .left_join(mod_transfer_community::table) - // The moderator - .left_join(person::table.on(moderator_names_join)) - // The comment - .left_join(comment::table.on(comment_join)) - // The post - .left_join(post::table.on(post_join)) - // The community - .left_join(community::table.on(community_join)) - // The instance - .left_join(instance::table.on(instance_join)) - // The other / modded person - .left_join(aliases::person1.on(other_person_join)) - .select(( - admin_allow_instance::all_columns.nullable(), - admin_block_instance::all_columns.nullable(), - admin_purge_comment::all_columns.nullable(), - admin_purge_community::all_columns.nullable(), - admin_purge_person::all_columns.nullable(), - admin_purge_post::all_columns.nullable(), - mod_add::all_columns.nullable(), - mod_add_community::all_columns.nullable(), - mod_ban::all_columns.nullable(), - mod_ban_from_community::all_columns.nullable(), - mod_feature_post::all_columns.nullable(), - mod_hide_community::all_columns.nullable(), - mod_lock_post::all_columns.nullable(), - mod_remove_comment::all_columns.nullable(), - mod_remove_community::all_columns.nullable(), - mod_remove_post::all_columns.nullable(), - mod_transfer_community::all_columns.nullable(), - // Shared - person::all_columns.nullable(), - aliases::person1.fields(person::all_columns).nullable(), - instance::all_columns.nullable(), - community::all_columns.nullable(), - post::all_columns.nullable(), - comment::all_columns.nullable(), - )) + .left_join(moderator_names_join) + .left_join(comment_join) + .left_join(post_join) + .left_join(community_join) + .left_join(instance_join) + .left_join(other_person_join) + .left_join(community_actions_join) + } +} + +impl PaginationCursorBuilder for ModlogCombinedView { + type CursorData = ModlogCombined; + fn to_cursor(&self) -> PaginationCursor { + let (prefix, id) = match &self { + ModlogCombinedView::AdminAllowInstance(v) => ('A', v.admin_allow_instance.id.0), + ModlogCombinedView::AdminBlockInstance(v) => ('B', v.admin_block_instance.id.0), + ModlogCombinedView::AdminPurgeComment(v) => ('C', v.admin_purge_comment.id.0), + ModlogCombinedView::AdminPurgeCommunity(v) => ('D', v.admin_purge_community.id.0), + ModlogCombinedView::AdminPurgePerson(v) => ('E', v.admin_purge_person.id.0), + ModlogCombinedView::AdminPurgePost(v) => ('F', v.admin_purge_post.id.0), + ModlogCombinedView::ModAdd(v) => ('G', v.mod_add.id.0), + ModlogCombinedView::ModAddCommunity(v) => ('H', v.mod_add_community.id.0), + ModlogCombinedView::ModBan(v) => ('I', v.mod_ban.id.0), + ModlogCombinedView::ModBanFromCommunity(v) => ('J', v.mod_ban_from_community.id.0), + ModlogCombinedView::ModFeaturePost(v) => ('K', v.mod_feature_post.id.0), + ModlogCombinedView::ModHideCommunity(v) => ('L', v.mod_hide_community.id.0), + ModlogCombinedView::ModLockPost(v) => ('M', v.mod_lock_post.id.0), + ModlogCombinedView::ModRemoveComment(v) => ('N', v.mod_remove_comment.id.0), + ModlogCombinedView::ModRemoveCommunity(v) => ('O', v.mod_remove_community.id.0), + ModlogCombinedView::ModRemovePost(v) => ('P', v.mod_remove_post.id.0), + ModlogCombinedView::ModTransferCommunity(v) => ('Q', v.mod_transfer_community.id.0), + }; + PaginationCursor::new(prefix, id) + } + + async fn from_cursor( + cursor: &PaginationCursor, + pool: &mut DbPool<'_>, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + let (prefix, id) = cursor.prefix_and_id()?; + + let mut query = modlog_combined::table + .select(Self::CursorData::as_select()) .into_boxed(); + query = match prefix { + 'A' => query.filter(modlog_combined::admin_allow_instance_id.eq(id)), + 'B' => query.filter(modlog_combined::admin_block_instance_id.eq(id)), + 'C' => query.filter(modlog_combined::admin_purge_comment_id.eq(id)), + 'D' => query.filter(modlog_combined::admin_purge_community_id.eq(id)), + 'E' => query.filter(modlog_combined::admin_purge_person_id.eq(id)), + 'F' => query.filter(modlog_combined::admin_purge_post_id.eq(id)), + 'G' => query.filter(modlog_combined::mod_add_id.eq(id)), + 'H' => query.filter(modlog_combined::mod_add_community_id.eq(id)), + 'I' => query.filter(modlog_combined::mod_ban_id.eq(id)), + 'J' => query.filter(modlog_combined::mod_ban_from_community_id.eq(id)), + 'K' => query.filter(modlog_combined::mod_feature_post_id.eq(id)), + 'L' => query.filter(modlog_combined::mod_hide_community_id.eq(id)), + 'M' => query.filter(modlog_combined::mod_lock_post_id.eq(id)), + 'N' => query.filter(modlog_combined::mod_remove_comment_id.eq(id)), + 'O' => query.filter(modlog_combined::mod_remove_community_id.eq(id)), + 'P' => query.filter(modlog_combined::mod_remove_post_id.eq(id)), + 'Q' => query.filter(modlog_combined::mod_transfer_community_id.eq(id)), + _ => return Err(LemmyErrorType::CouldntParsePaginationToken.into()), + }; + + let token = query.first(conn).await?; + + Ok(token) + } +} + +#[derive(Default)] +/// Querying / filtering the modlog. +pub struct ModlogCombinedQuery<'a> { + pub type_: Option, + pub listing_type: Option, + pub comment_id: Option, + pub post_id: Option, + pub community_id: Option, + pub hide_modlog_names: Option, + pub local_user: Option<&'a LocalUser>, + pub mod_person_id: Option, + pub other_person_id: Option, + pub cursor_data: Option, + pub page_back: Option, +} + +impl ModlogCombinedQuery<'_> { + pub async fn list(self, pool: &mut DbPool<'_>) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + let other_person = aliases::person1.field(person::id); + let my_person_id = self.local_user.person_id(); + + let mut query = + ModlogCombinedViewInternal::joins(self.mod_person_id, self.hide_modlog_names, my_person_id) + .select(ModlogCombinedViewInternal::as_select()) + .into_boxed(); + if let Some(mod_person_id) = self.mod_person_id { query = query.filter(person::id.eq(mod_person_id)); }; @@ -372,14 +367,22 @@ impl ModlogCombinedQuery { } } + let is_subscribed = community_actions::followed.is_not_null(); + query = match self.listing_type.unwrap_or(ListingType::All) { + ListingType::All => query, + ListingType::Subscribed => query.filter(is_subscribed), + ListingType::Local => query + .filter(community::local.eq(true)) + .filter(community::hidden.eq(false).or(is_subscribed)), + ListingType::ModeratorView => query.filter(community_actions::became_moderator.is_not_null()), + }; + let mut query = PaginatedQueryBuilder::new(query); - let page_after = self.page_after.map(|c| c.0); - if self.page_back.unwrap_or_default() { - query = query.before(page_after).limit_and_offset_from_end(); + query = query.before(self.cursor_data).limit_and_offset_from_end(); } else { - query = query.after(page_after); + query = query.after(self.cursor_data); } query = query @@ -592,7 +595,7 @@ impl InternalToCombinedView for ModlogCombinedViewInternal { #[expect(clippy::indexing_slicing)] mod tests { - use crate::{modlog_combined_view::ModlogCombinedQuery, structs::ModlogCombinedView}; + use crate::{combined::modlog_combined_view::ModlogCombinedQuery, structs::ModlogCombinedView}; use lemmy_db_schema::{ newtypes::PersonId, source::{ diff --git a/crates/db_views/src/person_content_combined_view.rs b/crates/db_views/src/combined/person_content_combined_view.rs similarity index 66% rename from crates/db_views/src/person_content_combined_view.rs rename to crates/db_views/src/combined/person_content_combined_view.rs index b20447b98..eb5397b04 100644 --- a/crates/db_views/src/person_content_combined_view.rs +++ b/crates/db_views/src/combined/person_content_combined_view.rs @@ -1,13 +1,11 @@ use crate::structs::{ CommentView, LocalUserView, - PersonContentCombinedPaginationCursor, PersonContentCombinedView, - PersonContentViewInternal, + PersonContentCombinedViewInternal, PostView, }; use diesel::{ - result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, @@ -18,12 +16,12 @@ use diesel::{ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ - aliases::creator_community_actions, - newtypes::PersonId, + aliases::{creator_community_actions, creator_local_user}, + impls::{community::community_follower_select_subscribed_type, local_user::local_user_can_mod}, + newtypes::{PaginationCursor, PersonId}, schema::{ comment, comment_actions, - comment_aggregates, community, community_actions, image_details, @@ -33,51 +31,137 @@ use lemmy_db_schema::{ person_content_combined, post, post_actions, - post_aggregates, post_tag, tag, }, - source::{ - combined::person_content::{person_content_combined_keys as key, PersonContentCombined}, - community::CommunityFollower, - }, - utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, - InternalToCombinedView, + source::combined::person_content::{person_content_combined_keys as key, PersonContentCombined}, + traits::{InternalToCombinedView, PaginationCursorBuilder}, + utils::{functions::coalesce, get_conn, DbPool}, PersonContentType, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; -impl PersonContentCombinedPaginationCursor { - // get cursor for page that starts immediately after the given post - pub fn after_post(view: &PersonContentCombinedView) -> PersonContentCombinedPaginationCursor { - let (prefix, id) = match view { - PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), - PersonContentCombinedView::Post(v) => ('P', v.post.id.0), - }; - // hex encoding to prevent ossification - PersonContentCombinedPaginationCursor(format!("{prefix}{id:x}")) - } +impl PersonContentCombinedViewInternal { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(my_person_id: Option) -> _ { + let item_creator = person::id; - pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { - let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); - let mut query = person_content_combined::table - .select(PersonContentCombined::as_select()) - .into_boxed(); - let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; - let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; - query = match prefix { - "C" => query.filter(person_content_combined::comment_id.eq(id)), - "P" => query.filter(person_content_combined::post_id.eq(id)), - _ => return Err(err_msg()), - }; - let token = query.first(&mut get_conn(pool).await?).await?; + let comment_join = + comment::table.on(person_content_combined::comment_id.eq(comment::id.nullable())); - Ok(PaginationCursorData(token)) + let post_join = post::table.on( + person_content_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.eq(post::id)), + ); + + let item_creator_join = person::table.on( + comment::creator_id + .eq(item_creator) + // Need to filter out the post rows where the post_id given is null + // Otherwise you'll get duped post rows + .or( + post::creator_id + .eq(item_creator) + .and(person_content_combined::post_id.is_not_null()), + ), + ); + + let community_join = community::table.on(post::community_id.eq(community::id)); + + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(item_creator), + ), + ); + let local_user_join = local_user::table.on(local_user::person_id.nullable().eq(my_person_id)); + + let creator_local_user_join = creator_local_user.on( + item_creator + .eq(creator_local_user.field(local_user::person_id)) + .and(creator_local_user.field(local_user::admin).eq(true)), + ); + + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(post::community_id) + .and(community_actions::person_id.nullable().eq(my_person_id)), + ); + + let post_actions_join = post_actions::table.on( + post_actions::post_id + .eq(post::id) + .and(post_actions::person_id.nullable().eq(my_person_id)), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(item_creator) + .and(person_actions::person_id.nullable().eq(my_person_id)), + ); + + let comment_actions_join = comment_actions::table.on( + comment_actions::comment_id + .eq(comment::id) + .and(comment_actions::person_id.nullable().eq(my_person_id)), + ); + + let image_details_join = + image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())); + + person_content_combined::table + .left_join(comment_join) + .inner_join(post_join) + .inner_join(item_creator_join) + .inner_join(community_join) + .left_join(creator_community_actions_join) + .left_join(local_user_join) + .left_join(creator_local_user_join) + .left_join(community_actions_join) + .left_join(post_actions_join) + .left_join(person_actions_join) + .left_join(comment_actions_join) + .left_join(image_details_join) } } -#[derive(Clone)] -pub struct PaginationCursorData(PersonContentCombined); +impl PaginationCursorBuilder for PersonContentCombinedView { + type CursorData = PersonContentCombined; + + fn to_cursor(&self) -> PaginationCursor { + let (prefix, id) = match &self { + PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), + PersonContentCombinedView::Post(v) => ('P', v.post.id.0), + }; + PaginationCursor::new(prefix, id) + } + + async fn from_cursor( + cursor: &PaginationCursor, + pool: &mut DbPool<'_>, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + let (prefix, id) = cursor.prefix_and_id()?; + + let mut query = person_content_combined::table + .select(Self::CursorData::as_select()) + .into_boxed(); + + query = match prefix { + 'C' => query.filter(person_content_combined::comment_id.eq(id)), + 'P' => query.filter(person_content_combined::post_id.eq(id)), + _ => return Err(LemmyErrorType::CouldntParsePaginationToken.into()), + }; + let token = query.first(conn).await?; + + Ok(token) + } +} #[derive(derive_new::new)] pub struct PersonContentCombinedQuery { @@ -85,7 +169,7 @@ pub struct PersonContentCombinedQuery { #[new(default)] pub type_: Option, #[new(default)] - pub page_after: Option, + pub cursor_data: Option, #[new(default)] pub page_back: Option, } @@ -115,71 +199,16 @@ impl PersonContentCombinedQuery { // For example, the creator must be the person table joined to either: // - post.creator_id // - comment.creator_id - let query = person_content_combined::table - // The comment - .left_join(comment::table.on(person_content_combined::comment_id.eq(comment::id.nullable()))) - // The post - // It gets a bit complicated here, because since both comments and post combined have a post - // attached, you can do an inner join. - .inner_join( - post::table.on( - person_content_combined::post_id - .eq(post::id.nullable()) - .or(comment::post_id.eq(post::id)), - ), - ) - // The item creator - .inner_join( - person::table.on( - comment::creator_id - .eq(item_creator) - // Need to filter out the post rows where the post_id given is null - // Otherwise you'll get duped post rows - .or( - post::creator_id - .eq(item_creator) - .and(person_content_combined::post_id.is_not_null()), - ), - ), - ) - // The community - .inner_join(community::table.on(post::community_id.eq(community::id))) - .left_join(actions_alias( - creator_community_actions, - item_creator, - post::community_id, - )) - .left_join( - local_user::table.on( - item_creator - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ) - .left_join(actions( - community_actions::table, - my_person_id, - post::community_id, - )) - .left_join(actions(post_actions::table, my_person_id, post::id)) - .left_join(actions(person_actions::table, my_person_id, item_creator)) - .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) - .left_join( - comment_aggregates::table - .on(person_content_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), - ) - .left_join(actions(comment_actions::table, my_person_id, comment::id)) - .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + let mut query = PersonContentCombinedViewInternal::joins(my_person_id) // The creator id filter .filter(item_creator.eq(self.creator_id)) .select(( // Post-specific - post_aggregates::all_columns, coalesce( - post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), - post_aggregates::comments, + post::comments.nullable() - post_actions::read_comments_amount.nullable(), + post::comments, ), - post_actions::saved.nullable().is_not_null(), + post_actions::saved.nullable(), post_actions::read.nullable().is_not_null(), post_actions::hidden.nullable().is_not_null(), post_actions::like_score.nullable(), @@ -187,15 +216,17 @@ impl PersonContentCombinedQuery { post_tags, // Comment-specific comment::all_columns.nullable(), - comment_aggregates::all_columns.nullable(), - comment_actions::saved.nullable().is_not_null(), + comment_actions::saved.nullable(), comment_actions::like_score.nullable(), // Shared post::all_columns, community::all_columns, person::all_columns, - CommunityFollower::select_subscribed_type(), - local_user::admin.nullable().is_not_null(), + community_follower_select_subscribed_type(), + creator_local_user + .field(local_user::admin) + .nullable() + .is_not_null(), creator_community_actions .field(community_actions::became_moderator) .nullable() @@ -206,11 +237,10 @@ impl PersonContentCombinedQuery { .is_not_null(), person_actions::blocked.nullable().is_not_null(), community_actions::received_ban.nullable().is_not_null(), + local_user_can_mod(), )) .into_boxed(); - let mut query = PaginatedQueryBuilder::new(query); - if let Some(type_) = self.type_ { query = match type_ { PersonContentType::All => query, @@ -221,12 +251,12 @@ impl PersonContentCombinedQuery { } } - let page_after = self.page_after.map(|c| c.0); + let mut query = PaginatedQueryBuilder::new(query); if self.page_back.unwrap_or_default() { - query = query.before(page_after).limit_and_offset_from_end(); + query = query.before(self.cursor_data).limit_and_offset_from_end(); } else { - query = query.after(page_after); + query = query.after(self.cursor_data); } // Sorting by published @@ -235,7 +265,9 @@ impl PersonContentCombinedQuery { // Tie breaker .then_desc(key::id); - let res = query.load::(conn).await?; + let res = query + .load::(conn) + .await?; // Map the query results to the enum let out = res @@ -247,17 +279,16 @@ impl PersonContentCombinedQuery { } } -impl InternalToCombinedView for PersonContentViewInternal { +impl InternalToCombinedView for PersonContentCombinedViewInternal { type CombinedView = PersonContentCombinedView; fn map_to_enum(self) -> Option { // Use for a short alias let v = self; - if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { + if let Some(comment) = v.comment { Some(PersonContentCombinedView::Comment(CommentView { comment, - counts, post: v.post, community: v.community, creator: v.item_creator, @@ -269,13 +300,13 @@ impl InternalToCombinedView for PersonContentViewInternal { saved: v.comment_saved, my_vote: v.my_comment_vote, banned_from_community: v.banned_from_community, + can_mod: v.can_mod, })) } else { Some(PersonContentCombinedView::Post(PostView { post: v.post, community: v.community, unread_comments: v.post_unread_comments, - counts: v.post_counts, creator: v.item_creator, creator_banned_from_community: v.item_creator_banned_from_community, creator_is_moderator: v.item_creator_is_moderator, @@ -289,6 +320,7 @@ impl InternalToCombinedView for PersonContentViewInternal { image_details: v.image_details, banned_from_community: v.banned_from_community, tags: v.post_tags, + can_mod: v.can_mod, })) } } @@ -299,7 +331,7 @@ impl InternalToCombinedView for PersonContentViewInternal { mod tests { use crate::{ - person_content_combined_view::PersonContentCombinedQuery, + combined::person_content_combined_view::PersonContentCombinedQuery, structs::PersonContentCombinedView, }; use lemmy_db_schema::{ diff --git a/crates/db_views/src/person_saved_combined_view.rs b/crates/db_views/src/combined/person_saved_combined_view.rs similarity index 54% rename from crates/db_views/src/person_saved_combined_view.rs rename to crates/db_views/src/combined/person_saved_combined_view.rs index 4e1ce8df7..197d44273 100644 --- a/crates/db_views/src/person_saved_combined_view.rs +++ b/crates/db_views/src/combined/person_saved_combined_view.rs @@ -1,11 +1,11 @@ use crate::structs::{ + CommentView, LocalUserView, - PersonContentCombinedView, - PersonContentViewInternal, - PersonSavedCombinedPaginationCursor, + PersonSavedCombinedView, + PersonSavedCombinedViewInternal, + PostView, }; use diesel::{ - result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, @@ -16,11 +16,12 @@ use diesel::{ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ - aliases::creator_community_actions, + aliases::{creator_community_actions, creator_local_user}, + impls::{community::community_follower_select_subscribed_type, local_user::local_user_can_mod}, + newtypes::{PaginationCursor, PersonId}, schema::{ comment, comment_actions, - comment_aggregates, community, community_actions, image_details, @@ -30,67 +31,153 @@ use lemmy_db_schema::{ person_saved_combined, post, post_actions, - post_aggregates, post_tag, tag, }, - source::{ - combined::person_saved::{person_saved_combined_keys as key, PersonSavedCombined}, - community::CommunityFollower, - }, - utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, - InternalToCombinedView, + source::combined::person_saved::{person_saved_combined_keys as key, PersonSavedCombined}, + traits::{InternalToCombinedView, PaginationCursorBuilder}, + utils::{functions::coalesce, get_conn, DbPool}, PersonContentType, }; -use lemmy_utils::error::LemmyResult; - -impl PersonSavedCombinedPaginationCursor { - // get cursor for page that starts immediately after the given post - pub fn after_post(view: &PersonContentCombinedView) -> PersonSavedCombinedPaginationCursor { - let (prefix, id) = match view { - PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), - PersonContentCombinedView::Post(v) => ('P', v.post.id.0), - }; - // hex encoding to prevent ossification - PersonSavedCombinedPaginationCursor(format!("{prefix}{id:x}")) - } - - pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { - let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); - let mut query = person_saved_combined::table - .select(PersonSavedCombined::as_select()) - .into_boxed(); - let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; - let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; - query = match prefix { - "C" => query.filter(person_saved_combined::comment_id.eq(id)), - "P" => query.filter(person_saved_combined::post_id.eq(id)), - _ => return Err(err_msg()), - }; - let token = query.first(&mut get_conn(pool).await?).await?; - - Ok(PaginationCursorData(token)) - } -} - -#[derive(Clone)] -pub struct PaginationCursorData(PersonSavedCombined); +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[derive(Default)] pub struct PersonSavedCombinedQuery { pub type_: Option, - pub page_after: Option, + pub cursor_data: Option, pub page_back: Option, } +impl PaginationCursorBuilder for PersonSavedCombinedView { + type CursorData = PersonSavedCombined; + + fn to_cursor(&self) -> PaginationCursor { + let (prefix, id) = match &self { + PersonSavedCombinedView::Comment(v) => ('C', v.comment.id.0), + PersonSavedCombinedView::Post(v) => ('P', v.post.id.0), + }; + PaginationCursor::new(prefix, id) + } + + async fn from_cursor( + cursor: &PaginationCursor, + pool: &mut DbPool<'_>, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + let (prefix, id) = cursor.prefix_and_id()?; + + let mut query = person_saved_combined::table + .select(Self::CursorData::as_select()) + .into_boxed(); + + query = match prefix { + 'C' => query.filter(person_saved_combined::comment_id.eq(id)), + 'P' => query.filter(person_saved_combined::post_id.eq(id)), + _ => return Err(LemmyErrorType::CouldntParsePaginationToken.into()), + }; + let token = query.first(conn).await?; + + Ok(token) + } +} + +impl PersonSavedCombinedViewInternal { + #[diesel::dsl::auto_type(no_type_alias)] + pub(crate) fn joins(my_person_id: PersonId) -> _ { + let item_creator = person::id; + + let comment_join = + comment::table.on(person_saved_combined::comment_id.eq(comment::id.nullable())); + + let post_join = post::table.on( + person_saved_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.eq(post::id)), + ); + + let item_creator_join = person::table.on( + comment::creator_id + .eq(item_creator) + // Need to filter out the post rows where the post_id given is null + // Otherwise you'll get duped post rows + .or( + post::creator_id + .eq(item_creator) + .and(person_saved_combined::post_id.is_not_null()), + ), + ); + + let community_join = community::table.on(post::community_id.eq(community::id)); + + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(item_creator), + ), + ); + + let local_user_join = local_user::table.on(local_user::person_id.nullable().eq(my_person_id)); + + let creator_local_user_join = creator_local_user.on( + item_creator + .eq(creator_local_user.field(local_user::person_id)) + .and(creator_local_user.field(local_user::admin).eq(true)), + ); + + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(post::community_id) + .and(community_actions::person_id.eq(my_person_id)), + ); + + let post_actions_join = post_actions::table.on( + post_actions::post_id + .eq(post::id) + .and(post_actions::person_id.eq(my_person_id)), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(item_creator) + .and(person_actions::person_id.eq(my_person_id)), + ); + + let comment_actions_join = comment_actions::table.on( + comment_actions::comment_id + .eq(comment::id) + .and(comment_actions::person_id.eq(my_person_id)), + ); + + let image_details_join = + image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())); + + person_saved_combined::table + .left_join(comment_join) + .inner_join(post_join) + .inner_join(item_creator_join) + .inner_join(community_join) + .left_join(creator_community_actions_join) + .left_join(local_user_join) + .left_join(creator_local_user_join) + .left_join(community_actions_join) + .left_join(post_actions_join) + .left_join(person_actions_join) + .left_join(comment_actions_join) + .left_join(image_details_join) + } +} + impl PersonSavedCombinedQuery { pub async fn list( self, pool: &mut DbPool<'_>, user: &LocalUserView, - ) -> LemmyResult> { + ) -> LemmyResult> { let my_person_id = user.local_user.person_id; - let item_creator = person::id; let conn = &mut get_conn(pool).await?; @@ -103,84 +190,15 @@ impl PersonSavedCombinedQuery { .filter(tag::deleted.eq(false)) .single_value(); - // Notes: since the post_id and comment_id are optional columns, - // many joins must use an OR condition. - // For example, the creator must be the person table joined to either: - // - post.creator_id - // - comment.creator_id - let query = person_saved_combined::table - // The comment - .left_join(comment::table.on(person_saved_combined::comment_id.eq(comment::id.nullable()))) - // The post - // It gets a bit complicated here, because since both comments and post combined have a post - // attached, you can do an inner join. - .inner_join( - post::table.on( - person_saved_combined::post_id - .eq(post::id.nullable()) - .or(comment::post_id.eq(post::id)), - ), - ) - // The item creator - .inner_join( - person::table.on( - comment::creator_id - .eq(item_creator) - // Need to filter out the post rows where the post_id given is null - // Otherwise you'll get duped post rows - .or( - post::creator_id - .eq(item_creator) - .and(person_saved_combined::post_id.is_not_null()), - ), - ), - ) - // The community - .inner_join(community::table.on(post::community_id.eq(community::id))) - .left_join(actions_alias( - creator_community_actions, - item_creator, - post::community_id, - )) - .left_join( - local_user::table.on( - item_creator - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ) - .left_join(actions( - community_actions::table, - Some(my_person_id), - post::community_id, - )) - .left_join(actions(post_actions::table, Some(my_person_id), post::id)) - .left_join(actions( - person_actions::table, - Some(my_person_id), - item_creator, - )) - .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) - .left_join( - comment_aggregates::table - .on(person_saved_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), - ) - .left_join(actions( - comment_actions::table, - Some(my_person_id), - comment::id, - )) - .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) - // The person id filter + let mut query = PersonSavedCombinedViewInternal::joins(my_person_id) .filter(person_saved_combined::person_id.eq(my_person_id)) .select(( // Post-specific - post_aggregates::all_columns, coalesce( - post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), - post_aggregates::comments, + post::comments.nullable() - post_actions::read_comments_amount.nullable(), + post::comments, ), - post_actions::saved.nullable().is_not_null(), + post_actions::saved.nullable(), post_actions::read.nullable().is_not_null(), post_actions::hidden.nullable().is_not_null(), post_actions::like_score.nullable(), @@ -188,15 +206,17 @@ impl PersonSavedCombinedQuery { post_tags, // Comment-specific comment::all_columns.nullable(), - comment_aggregates::all_columns.nullable(), - comment_actions::saved.nullable().is_not_null(), + comment_actions::saved.nullable(), comment_actions::like_score.nullable(), // Shared post::all_columns, community::all_columns, person::all_columns, - CommunityFollower::select_subscribed_type(), - local_user::admin.nullable().is_not_null(), + community_follower_select_subscribed_type(), + creator_local_user + .field(local_user::admin) + .nullable() + .is_not_null(), creator_community_actions .field(community_actions::became_moderator) .nullable() @@ -207,11 +227,10 @@ impl PersonSavedCombinedQuery { .is_not_null(), person_actions::blocked.nullable().is_not_null(), community_actions::received_ban.nullable().is_not_null(), + local_user_can_mod(), )) .into_boxed(); - let mut query = PaginatedQueryBuilder::new(query); - if let Some(type_) = self.type_ { query = match type_ { PersonContentType::All => query, @@ -222,12 +241,12 @@ impl PersonSavedCombinedQuery { } } - let page_after = self.page_after.map(|c| c.0); + let mut query = PaginatedQueryBuilder::new(query); if self.page_back.unwrap_or_default() { - query = query.before(page_after).limit_and_offset_from_end(); + query = query.before(self.cursor_data).limit_and_offset_from_end(); } else { - query = query.after(page_after); + query = query.after(self.cursor_data); } // Sorting by saved desc @@ -236,7 +255,7 @@ impl PersonSavedCombinedQuery { // Tie breaker .then_desc(key::id); - let res = query.load::(conn).await?; + let res = query.load::(conn).await?; // Map the query results to the enum let out = res @@ -248,13 +267,60 @@ impl PersonSavedCombinedQuery { } } +impl InternalToCombinedView for PersonSavedCombinedViewInternal { + type CombinedView = PersonSavedCombinedView; + + fn map_to_enum(self) -> Option { + // Use for a short alias + let v = self; + + if let Some(comment) = v.comment { + Some(PersonSavedCombinedView::Comment(CommentView { + comment, + post: v.post, + community: v.community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + can_mod: v.can_mod, + })) + } else { + Some(PersonSavedCombinedView::Post(PostView { + post: v.post, + community: v.community, + unread_comments: v.post_unread_comments, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + image_details: v.image_details, + banned_from_community: v.banned_from_community, + tags: v.post_tags, + can_mod: v.can_mod, + })) + } + } +} + #[cfg(test)] #[expect(clippy::indexing_slicing)] mod tests { use crate::{ - person_saved_combined_view::PersonSavedCombinedQuery, - structs::{LocalUserView, PersonContentCombinedView}, + combined::person_saved_combined_view::PersonSavedCombinedQuery, + structs::{LocalUserView, PersonSavedCombinedView}, }; use lemmy_db_schema::{ source::{ @@ -262,7 +328,6 @@ mod tests { community::{Community, CommunityInsertForm}, instance::Instance, local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm, PostSaved, PostSavedForm}, }, @@ -292,9 +357,7 @@ mod tests { let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; let timmy_view = LocalUserView { local_user: timmy_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), person: timmy.clone(), - counts: Default::default(), }; let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); @@ -376,19 +439,19 @@ mod tests { assert_eq!(3, timmy_saved.len()); // Make sure the types and order are correct - if let PersonContentCombinedView::Post(v) = &timmy_saved[0] { + if let PersonSavedCombinedView::Post(v) = &timmy_saved[0] { assert_eq!(data.timmy_post.id, v.post.id); assert_eq!(data.timmy.id, v.post.creator_id); } else { panic!("wrong type"); } - if let PersonContentCombinedView::Comment(v) = &timmy_saved[1] { + if let PersonSavedCombinedView::Comment(v) = &timmy_saved[1] { assert_eq!(data.sara_comment.id, v.comment.id); assert_eq!(data.sara.id, v.comment.creator_id); } else { panic!("wrong type"); } - if let PersonContentCombinedView::Comment(v) = &timmy_saved[2] { + if let PersonSavedCombinedView::Comment(v) = &timmy_saved[2] { assert_eq!(data.sara_comment_2.id, v.comment.id); assert_eq!(data.sara.id, v.comment.creator_id); } else { @@ -404,7 +467,7 @@ mod tests { .await?; assert_eq!(1, timmy_saved.len()); - if let PersonContentCombinedView::Comment(v) = &timmy_saved[0] { + if let PersonSavedCombinedView::Comment(v) = &timmy_saved[0] { assert_eq!(data.sara_comment_2.id, v.comment.id); assert_eq!(data.sara.id, v.comment.creator_id); } else { diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/combined/report_combined_view.rs similarity index 64% rename from crates/db_views/src/report_combined_view.rs rename to crates/db_views/src/combined/report_combined_view.rs index c082d7f74..d929b66a2 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/combined/report_combined_view.rs @@ -4,10 +4,10 @@ use crate::structs::{ LocalUserView, PostReportView, PrivateMessageReportView, - ReportCombinedPaginationCursor, ReportCombinedView, ReportCombinedViewInternal, }; +use chrono::{DateTime, Days, Utc}; use diesel::{ result::Error, BoolExpressionMethods, @@ -22,37 +22,139 @@ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::CommunityId, + impls::community::community_follower_select_subscribed_type, + newtypes::{CommunityId, PaginationCursor, PersonId, PostId}, schema::{ comment, comment_actions, - comment_aggregates, comment_report, community, community_actions, - community_aggregates, community_report, local_user, person, person_actions, post, post_actions, - post_aggregates, post_report, private_message, private_message_report, report_combined, }, - source::{ - combined::report::{report_combined_keys as key, ReportCombined}, - community::CommunityFollower, - }, - utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, - InternalToCombinedView, + source::combined::report::{report_combined_keys as key, ReportCombined}, + traits::{InternalToCombinedView, PaginationCursorBuilder}, + utils::{functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, + ReportType, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl ReportCombinedViewInternal { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(my_person_id: PersonId) -> _ { + let report_creator = person::id; + let item_creator = aliases::person1.field(person::id); + let resolver = aliases::person2.field(person::id).nullable(); + + let comment_join = comment::table.on(comment_report::comment_id.eq(comment::id)); + let private_message_join = + private_message::table.on(private_message_report::private_message_id.eq(private_message::id)); + + let post_join = post::table.on( + post_report::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)), + ); + + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(community::id) + .and(community_actions::person_id.eq(my_person_id)), + ); + + let report_creator_join = person::table.on( + post_report::creator_id + .eq(report_creator) + .or(comment_report::creator_id.eq(report_creator)) + .or(private_message_report::creator_id.eq(report_creator)) + .or(community_report::creator_id.eq(report_creator)), + ); + + let item_creator_join = aliases::person1.on( + post::creator_id + .eq(item_creator) + .or(comment::creator_id.eq(item_creator)) + .or(private_message::creator_id.eq(item_creator)), + ); + + let resolver_join = aliases::person2.on( + private_message_report::resolver_id + .eq(resolver) + .or(post_report::resolver_id.eq(resolver)) + .or(comment_report::resolver_id.eq(resolver)) + .or(community_report::resolver_id.eq(resolver)), + ); + + let community_join = community::table.on( + community_report::community_id + .eq(community::id) + .or(post::community_id.eq(community::id)), + ); + + let local_user_join = local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ); + + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(item_creator), + ), + ); + + let post_actions_join = post_actions::table.on( + post_actions::post_id + .eq(post::id) + .and(post_actions::person_id.eq(my_person_id)), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(item_creator) + .and(person_actions::person_id.eq(my_person_id)), + ); + + let comment_actions_join = comment_actions::table.on( + comment_actions::comment_id + .eq(comment::id) + .and(comment_actions::person_id.eq(my_person_id)), + ); + + report_combined::table + .left_join(post_report::table) + .left_join(comment_report::table) + .left_join(private_message_report::table) + .left_join(community_report::table) + .inner_join(report_creator_join) + .left_join(comment_join) + .left_join(private_message_join) + .left_join(post_join) + .left_join(item_creator_join) + .left_join(resolver_join) + .left_join(community_join) + .left_join(creator_community_actions_join) + .left_join(local_user_join) + .left_join(community_actions_join) + .left_join(post_actions_join) + .left_join(person_actions_join) + .left_join(comment_actions_join) + } + /// returns the current unresolved report count for the communities you mod pub async fn get_report_count( pool: &mut DbPool<'_>, @@ -64,27 +166,7 @@ impl ReportCombinedViewInternal { let conn = &mut get_conn(pool).await?; let my_person_id = user.local_user.person_id; - let mut query = report_combined::table - .left_join(post_report::table) - .left_join(comment_report::table) - .left_join(private_message_report::table) - .left_join(community_report::table) - // Need to join to comment and post to get the community - .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) - // The post - .left_join( - post::table.on( - post_report::post_id - .eq(post::id) - .or(comment::post_id.eq(post::id)), - ), - ) - .left_join(community::table.on(post::community_id.eq(community::id))) - .left_join(actions( - community_actions::table, - Some(my_person_id), - post::community_id, - )) + let mut query = Self::joins(my_person_id) .filter( post_report::resolved .or(comment_report::resolved) @@ -92,65 +174,74 @@ impl ReportCombinedViewInternal { .or(community_report::resolved) .is_distinct_from(true), ) + .select(count(report_combined::id)) .into_boxed(); if let Some(community_id) = community_id { - query = query.filter(post::community_id.eq(community_id)) + query = query.filter( + community::id + .eq(community_id) + .and(report_combined::community_report_id.is_null()), + ); } - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); + if user.local_user.admin { + query = query.filter(filter_admin_reports(Utc::now() - Days::new(3))); + } else { + query = query.filter(filter_mod_reports()); } - query - .select(count(report_combined::id)) - .first::(conn) - .await + query.first::(conn).await } } -impl ReportCombinedPaginationCursor { - // get cursor for page that starts immediately after the given post - pub fn after_post(view: &ReportCombinedView) -> ReportCombinedPaginationCursor { - let (prefix, id) = match view { +impl PaginationCursorBuilder for ReportCombinedView { + type CursorData = ReportCombined; + + fn to_cursor(&self) -> PaginationCursor { + let (prefix, id) = match &self { ReportCombinedView::Comment(v) => ('C', v.comment_report.id.0), ReportCombinedView::Post(v) => ('P', v.post_report.id.0), ReportCombinedView::PrivateMessage(v) => ('M', v.private_message_report.id.0), ReportCombinedView::Community(v) => ('Y', v.community_report.id.0), }; - // hex encoding to prevent ossification - ReportCombinedPaginationCursor(format!("{prefix}{id:x}")) + PaginationCursor::new(prefix, id) } - pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { - let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); - let mut query = report_combined::table - .select(ReportCombined::as_select()) - .into_boxed(); - let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; - let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; - query = match prefix { - "C" => query.filter(report_combined::comment_report_id.eq(id)), - "P" => query.filter(report_combined::post_report_id.eq(id)), - "M" => query.filter(report_combined::private_message_report_id.eq(id)), - "Y" => query.filter(report_combined::community_report_id.eq(id)), - _ => return Err(err_msg()), - }; - let token = query.first(&mut get_conn(pool).await?).await?; + async fn from_cursor( + cursor: &PaginationCursor, + pool: &mut DbPool<'_>, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + let (prefix, id) = cursor.prefix_and_id()?; - Ok(PaginationCursorData(token)) + let mut query = report_combined::table + .select(Self::CursorData::as_select()) + .into_boxed(); + + query = match prefix { + 'C' => query.filter(report_combined::comment_report_id.eq(id)), + 'P' => query.filter(report_combined::post_report_id.eq(id)), + 'M' => query.filter(report_combined::private_message_report_id.eq(id)), + 'Y' => query.filter(report_combined::community_report_id.eq(id)), + _ => return Err(LemmyErrorType::CouldntParsePaginationToken.into()), + }; + let token = query.first(conn).await?; + + Ok(token) } } -#[derive(Clone)] -pub struct PaginationCursorData(ReportCombined); - #[derive(Default)] pub struct ReportCombinedQuery { + pub type_: Option, + pub post_id: Option, pub community_id: Option, pub unresolved_only: Option, - pub page_after: Option, + /// For admins, also show reports with `violates_instance_rules=false` + pub show_community_rule_violations: Option, + pub cursor_data: Option, + pub my_reports_only: Option, pub page_back: Option, } @@ -161,141 +252,37 @@ impl ReportCombinedQuery { user: &LocalUserView, ) -> LemmyResult> { let my_person_id = user.local_user.person_id; - let report_creator = person::id; - let item_creator = aliases::person1.field(person::id); - let resolver = aliases::person2.field(person::id).nullable(); let conn = &mut get_conn(pool).await?; - - // Notes: since the post_report_id and comment_report_id are optional columns, - // many joins must use an OR condition. - // For example, the report creator must be the person table joined to either: - // - post_report.creator_id - // - comment_report.creator_id - let mut query = report_combined::table - .left_join(post_report::table) - .left_join(comment_report::table) - .left_join(private_message_report::table) - .left_join(community_report::table) - // The report creator - .inner_join( - person::table.on( - post_report::creator_id - .eq(report_creator) - .or(comment_report::creator_id.eq(report_creator)) - .or(private_message_report::creator_id.eq(report_creator)) - .or(community_report::creator_id.eq(report_creator)), - ), - ) - // The comment - .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) - // The private message - .left_join( - private_message::table - .on(private_message_report::private_message_id.eq(private_message::id)), - ) - // The post - .left_join( - post::table.on( - post_report::post_id - .eq(post::id) - .or(comment::post_id.eq(post::id)), - ), - ) - // The item creator (`item_creator` is the id of this person) - .left_join( - aliases::person1.on( - post::creator_id - .eq(item_creator) - .or(comment::creator_id.eq(item_creator)) - .or(private_message::creator_id.eq(item_creator)), - ), - ) - // The community - .left_join( - community::table.on( - post::community_id - .eq(community::id) - .or(community_report::community_id.eq(community::id)), - ), - ) - .left_join(actions_alias( - creator_community_actions, - item_creator, - post::community_id, - )) - .left_join( - local_user::table.on( - item_creator - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ) - .left_join(actions( - community_actions::table, - Some(my_person_id), - community::id, - )) - .left_join(actions(post_actions::table, Some(my_person_id), post::id)) - .left_join(actions( - person_actions::table, - Some(my_person_id), - item_creator, - )) - .left_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) - .left_join( - comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), - ) - .left_join( - community_aggregates::table - .on(community_report::community_id.eq(community_aggregates::community_id)), - ) - // The resolver - .left_join( - aliases::person2.on( - private_message_report::resolver_id - .eq(resolver) - .or(post_report::resolver_id.eq(resolver)) - .or(comment_report::resolver_id.eq(resolver)) - .or(community_report::resolver_id.eq(resolver)), - ), - ) - .left_join(actions( - comment_actions::table, - Some(my_person_id), - comment_report::comment_id, - )) + let mut query = ReportCombinedViewInternal::joins(my_person_id) .select(( // Post-specific post_report::all_columns.nullable(), post::all_columns.nullable(), - post_aggregates::all_columns.nullable(), coalesce( - post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), - post_aggregates::comments, + post::comments.nullable() - post_actions::read_comments_amount.nullable(), + post::comments, ) .nullable(), - post_actions::saved.nullable().is_not_null(), + post_actions::saved.nullable(), post_actions::read.nullable().is_not_null(), post_actions::hidden.nullable().is_not_null(), post_actions::like_score.nullable(), // Comment-specific comment_report::all_columns.nullable(), comment::all_columns.nullable(), - comment_aggregates::all_columns.nullable(), - comment_actions::saved.nullable().is_not_null(), + comment_actions::saved.nullable(), comment_actions::like_score.nullable(), // Private-message-specific private_message_report::all_columns.nullable(), private_message::all_columns.nullable(), // Community-specific community_report::all_columns.nullable(), - community_aggregates::all_columns.nullable(), // Shared person::all_columns, aliases::person1.fields(person::all_columns.nullable()), community::all_columns.nullable(), - CommunityFollower::select_subscribed_type(), + community_follower_select_subscribed_type(), aliases::person2.fields(person::all_columns.nullable()), local_user::admin.nullable().is_not_null(), creator_community_actions @@ -318,23 +305,41 @@ impl ReportCombinedQuery { ); } - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter( - community_actions::became_moderator - .is_not_null() - .and(report_combined::community_report_id.is_null()), - ); + if user.local_user.admin { + let show_community_rule_violations = self.show_community_rule_violations.unwrap_or_default(); + if !show_community_rule_violations { + query = query.filter(filter_admin_reports(Utc::now() - Days::new(3))); + } + } else { + query = query.filter(filter_mod_reports()); + } + + if let Some(post_id) = self.post_id { + query = query.filter(post::id.eq(post_id)); + } + + if self.my_reports_only.unwrap_or_default() { + query = query.filter(person::id.eq(my_person_id)); } let mut query = PaginatedQueryBuilder::new(query); - let page_after = self.page_after.map(|c| c.0); - if self.page_back.unwrap_or_default() { - query = query.before(page_after).limit_and_offset_from_end(); + query = query.before(self.cursor_data).limit_and_offset_from_end(); } else { - query = query.after(page_after); + query = query.after(self.cursor_data); + } + + if let Some(type_) = self.type_ { + query = match type_ { + ReportType::All => query, + ReportType::Posts => query.filter(report_combined::post_report_id.is_not_null()), + ReportType::Comments => query.filter(report_combined::comment_report_id.is_not_null()), + ReportType::PrivateMessages => { + query.filter(report_combined::private_message_report_id.is_not_null()) + } + ReportType::Communities => query.filter(report_combined::community_report_id.is_not_null()), + } } // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest @@ -370,6 +375,38 @@ impl ReportCombinedQuery { } } +/// Mods can only see reports for posts/comments inside of communities where they are moderator, +/// and which have `violates_instance_rules == false`. +#[diesel::dsl::auto_type] +fn filter_mod_reports() -> _ { + community_actions::became_moderator + .is_not_null() + // Reporting a community or private message must go to admins + .and(report_combined::community_report_id.is_null()) + .and(report_combined::private_message_report_id.is_null()) + .and(filter_violates_instance_rules().is_distinct_from(true)) +} + +/// Admins can see reports intended for them, or mod reports older than 3 days. Also reports +/// on communities, person and private messages. +#[diesel::dsl::auto_type] +fn filter_admin_reports(interval: DateTime) -> _ { + filter_violates_instance_rules() + .or(report_combined::published.lt(interval)) + // Also show community reports where the admin is a community mod + .or(community_actions::became_moderator.is_not_null()) +} + +/// Filter reports which are only for admins (either post/comment report with +/// `violates_instance_rules=true`, or report on a community/person/private message. +#[diesel::dsl::auto_type] +fn filter_violates_instance_rules() -> _ { + post_report::violates_instance_rules + .or(comment_report::violates_instance_rules) + .or(report_combined::community_report_id.is_not_null()) + .or(report_combined::private_message_report_id.is_not_null()) +} + impl InternalToCombinedView for ReportCombinedViewInternal { type CombinedView = ReportCombinedView; @@ -382,14 +419,12 @@ impl InternalToCombinedView for ReportCombinedViewInternal { Some(post), Some(community), Some(unread_comments), - Some(counts), Some(post_creator), ) = ( v.post_report, v.post.clone(), v.community.clone(), v.post_unread_comments, - v.post_counts, v.item_creator.clone(), ) { Some(ReportCombinedView::Post(PostReportView { @@ -397,7 +432,6 @@ impl InternalToCombinedView for ReportCombinedViewInternal { post, community, unread_comments, - counts, creator: v.report_creator, post_creator, creator_banned_from_community: v.item_creator_banned_from_community, @@ -414,14 +448,12 @@ impl InternalToCombinedView for ReportCombinedViewInternal { } else if let ( Some(comment_report), Some(comment), - Some(counts), Some(post), Some(community), Some(comment_creator), ) = ( v.comment_report, v.comment, - v.comment_counts, v.post, v.community.clone(), v.item_creator.clone(), @@ -429,7 +461,6 @@ impl InternalToCombinedView for ReportCombinedViewInternal { Some(ReportCombinedView::Comment(CommentReportView { comment_report, comment, - counts, post, community, creator: v.report_creator, @@ -458,14 +489,11 @@ impl InternalToCombinedView for ReportCombinedViewInternal { resolver: v.resolver, }, )) - } else if let (Some(community), Some(community_report), Some(counts)) = - (v.community, v.community_report, v.community_counts) - { + } else if let (Some(community), Some(community_report)) = (v.community, v.community_report) { Some(ReportCombinedView::Community(CommunityReportView { community_report, community, creator: v.report_creator, - counts, subscribed: v.subscribed, resolver: v.resolver, })) @@ -480,18 +508,22 @@ impl InternalToCombinedView for ReportCombinedViewInternal { mod tests { use crate::{ - report_combined_view::ReportCombinedQuery, + combined::report_combined_view::ReportCombinedQuery, structs::{ CommentReportView, + CommunityReportView, LocalUserView, PostReportView, ReportCombinedView, ReportCombinedViewInternal, }, }; + use chrono::{Days, Utc}; + use diesel::{update, ExpressionMethods, QueryDsl}; + use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aggregates::structs::{CommentAggregates, PostAggregates}, assert_length, + schema::report_combined, source::{ comment::{Comment, CommentInsertForm}, comment_report::{CommentReport, CommentReportForm}, @@ -499,7 +531,6 @@ mod tests { community_report::{CommunityReport, CommunityReportForm}, instance::Instance, local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm}, post_report::{PostReport, PostReportForm}, @@ -507,7 +538,8 @@ mod tests { private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, }, traits::{Crud, Joinable, Reportable}, - utils::{build_db_pool_for_tests, DbPool}, + utils::{build_db_pool_for_tests, get_conn, DbPool}, + ReportType, }; use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; @@ -535,9 +567,7 @@ mod tests { let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; let timmy_view = LocalUserView { local_user: timmy_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), person: inserted_timmy.clone(), - counts: Default::default(), }; // Make an admin, to be able to see private message reports. @@ -547,9 +577,7 @@ mod tests { let admin_local_user = LocalUser::create(pool, &admin_local_user_form, vec![]).await?; let admin_view = LocalUserView { local_user: admin_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), person: inserted_admin.clone(), - counts: Default::default(), }; let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rcv"); @@ -617,7 +645,7 @@ mod tests { #[tokio::test] #[serial] - async fn test_combined() -> LemmyResult<()> { + async fn combined() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -644,6 +672,7 @@ mod tests { original_post_url: None, original_post_body: None, reason: "from sara".into(), + violates_instance_rules: false, }; let inserted_post_report = PostReport::report(pool, &sara_report_post_form).await?; @@ -653,6 +682,7 @@ mod tests { comment_id: data.comment.id, original_comment_text: "A test comment rv".into(), reason: "from sara".into(), + violates_instance_rules: false, }; CommentReport::report(pool, &sara_report_comment_form).await?; @@ -674,10 +704,13 @@ mod tests { PrivateMessageReport::report(pool, &pm_report_form).await?; // Do a batch read of admins reports - let reports = ReportCombinedQuery::default() - .list(pool, &data.admin_view) - .await?; - assert_eq!(4, reports.len()); + let reports = ReportCombinedQuery { + show_community_rule_violations: Some(true), + ..Default::default() + } + .list(pool, &data.admin_view) + .await?; + assert_length!(4, reports); // Make sure the report types are correct if let ReportCombinedView::Community(v) = &reports[3] { @@ -705,26 +738,48 @@ mod tests { panic!("wrong type"); } + let report_count_mod = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count_mod); let report_count_admin = ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?; - assert_eq!(4, report_count_admin); + assert_eq!(2, report_count_admin); + + // Make sure the type_ filter is working + let reports_by_type = ReportCombinedQuery { + type_: Some(ReportType::Posts), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + assert_length!(1, reports_by_type); + + // Filter by the post id + // Should be 2, for the post, and the comment on that post + let reports_by_post_id = ReportCombinedQuery { + post_id: Some(data.post.id), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + assert_length!(2, reports_by_post_id); // Timmy should only see 2 reports, since they're not an admin, // but they do mod the community - let reports = ReportCombinedQuery::default() + let timmys_reports = ReportCombinedQuery::default() .list(pool, &data.timmy_view) .await?; - assert_eq!(2, reports.len()); + assert_length!(2, timmys_reports); // Make sure the report types are correct - if let ReportCombinedView::Post(v) = &reports[1] { + if let ReportCombinedView::Post(v) = &timmys_reports[1] { assert_eq!(data.post.id, v.post.id); assert_eq!(data.sara.id, v.creator.id); assert_eq!(data.timmy.id, v.post_creator.id); } else { panic!("wrong type"); } - if let ReportCombinedView::Comment(v) = &reports[0] { + if let ReportCombinedView::Comment(v) = &timmys_reports[0] { assert_eq!(data.comment.id, v.comment.id); assert_eq!(data.post.id, v.post.id); assert_eq!(data.timmy.id, v.comment_creator.id); @@ -761,7 +816,7 @@ mod tests { #[tokio::test] #[serial] - async fn test_private_message_reports() -> LemmyResult<()> { + async fn private_message_reports() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -783,9 +838,12 @@ mod tests { }; let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; - let reports = ReportCombinedQuery::default() - .list(pool, &data.admin_view) - .await?; + let reports = ReportCombinedQuery { + show_community_rule_violations: Some(true), + ..Default::default() + } + .list(pool, &data.admin_view) + .await?; assert_length!(1, reports); if let ReportCombinedView::PrivateMessage(v) = &reports[0] { assert!(!v.private_message_report.resolved); @@ -822,7 +880,7 @@ mod tests { #[tokio::test] #[serial] - async fn test_post_reports() -> LemmyResult<()> { + async fn post_reports() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -835,6 +893,7 @@ mod tests { original_post_url: None, original_post_body: None, reason: "from sara".into(), + violates_instance_rules: false, }; PostReport::report(pool, &sara_report_form).await?; @@ -847,6 +906,7 @@ mod tests { original_post_url: None, original_post_body: None, reason: "from jessica".into(), + violates_instance_rules: false, }; let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; @@ -855,14 +915,14 @@ mod tests { PostReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; // Make sure the triggers are reading the aggregates correctly. - let agg_1 = PostAggregates::read(pool, data.post.id).await?; - let agg_2 = PostAggregates::read(pool, data.post_2.id).await?; + let agg_1 = Post::read(pool, data.post.id).await?; + let agg_2 = Post::read(pool, data.post_2.id).await?; assert_eq!( read_jessica_report_view.post_report, inserted_jessica_report ); - assert_eq!(read_jessica_report_view.post, data.post_2); + assert_eq!(read_jessica_report_view.post.id, data.post_2.id); assert_eq!(read_jessica_report_view.community.id, data.community.id); assert_eq!(read_jessica_report_view.creator.id, data.jessica.id); assert_eq!(read_jessica_report_view.post_creator.id, data.timmy.id); @@ -916,12 +976,12 @@ mod tests { ); // Make sure the unresolved_post report got decremented in the trigger - let agg_2 = PostAggregates::read(pool, data.post_2.id).await?; + let agg_2 = Post::read(pool, data.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, data.post.id).await?; + let agg_1 = Post::read(pool, data.post.id).await?; assert_eq!(agg_1.report_count, 1); assert_eq!(agg_1.unresolved_report_count, 1); @@ -953,7 +1013,7 @@ mod tests { #[tokio::test] #[serial] - async fn test_comment_reports() -> LemmyResult<()> { + async fn comment_reports() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -964,6 +1024,7 @@ mod tests { comment_id: data.comment.id, original_comment_text: "this was it at time of creation".into(), reason: "from sara".into(), + violates_instance_rules: false, }; CommentReport::report(pool, &sara_report_form).await?; @@ -974,16 +1035,17 @@ mod tests { comment_id: data.comment.id, original_comment_text: "this was it at time of creation".into(), reason: "from jessica".into(), + violates_instance_rules: false, }; let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; - let agg = CommentAggregates::read(pool, data.comment.id).await?; - assert_eq!(agg.report_count, 2); + let comment = Comment::read(pool, data.comment.id).await?; + assert_eq!(comment.report_count, 2); let read_jessica_report_view = CommentReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; - assert_eq!(read_jessica_report_view.counts.unresolved_report_count, 2); + assert_eq!(read_jessica_report_view.comment.unresolved_report_count, 2); // Do a batch read of timmys reports let reports = ReportCombinedQuery::default() @@ -1050,6 +1112,16 @@ mod tests { ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; assert_eq!(1, report_count_after_resolved); + // Filter by post id, which should still include the comments. + let reports_post_id_filter = ReportCombinedQuery { + post_id: Some(data.post.id), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + + assert_length!(2, reports_post_id_filter); + cleanup(data, pool).await?; Ok(()) @@ -1057,7 +1129,7 @@ mod tests { #[tokio::test] #[serial] - async fn test_community_reports() -> LemmyResult<()> { + async fn community_reports() -> LemmyResult<()> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1076,9 +1148,12 @@ mod tests { }; let community_report = CommunityReport::report(pool, &community_report_form).await?; - let reports = ReportCombinedQuery::default() - .list(pool, &data.admin_view) - .await?; + let reports = ReportCombinedQuery { + show_community_rule_violations: Some(true), + ..Default::default() + } + .list(pool, &data.admin_view) + .await?; assert_length!(1, reports); if let ReportCombinedView::Community(v) = &reports[0] { assert!(!v.community_report.resolved); @@ -1086,6 +1161,9 @@ mod tests { assert_eq!(community_report.reason, v.community_report.reason); assert_eq!(data.community.name, v.community.name); assert_eq!(data.community.title, v.community.title); + let read_report = + CommunityReportView::read(pool, community_report.id, data.admin_view.person.id).await?; + assert_eq!(&read_report, v); } else { panic!("wrong type"); } @@ -1093,9 +1171,12 @@ mod tests { // admin resolves the report (after taking appropriate action) CommunityReport::resolve(pool, community_report.id, data.admin_view.person.id).await?; - let reports = ReportCombinedQuery::default() - .list(pool, &data.admin_view) - .await?; + let reports = ReportCombinedQuery { + show_community_rule_violations: Some(true), + ..Default::default() + } + .list(pool, &data.admin_view) + .await?; assert_length!(1, reports); if let ReportCombinedView::Community(v) = &reports[0] { assert!(v.community_report.resolved); @@ -1112,4 +1193,145 @@ mod tests { Ok(()) } + + #[tokio::test] + #[serial] + async fn violates_instance_rules() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // create report to admins + let report_form = PostReportForm { + creator_id: data.sara.id, + post_id: data.post_2.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from sara".into(), + violates_instance_rules: true, + }; + PostReport::report(pool, &report_form).await?; + + // timmy is a mod and cannot see the report + let mod_reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_length!(0, mod_reports); + let count = ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(0, count); + + // only admin can see the report + let admin_reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, admin_reports); + let count = ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?; + assert_eq!(1, count); + + // cleanup the report for easier checks below + Post::delete(pool, data.post_2.id).await?; + + // now create a mod report + let report_form = CommentReportForm { + creator_id: data.sara.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from sara".into(), + violates_instance_rules: false, + }; + let comment_report = CommentReport::report(pool, &report_form).await?; + + // this time the mod can see it + let mod_reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_length!(1, mod_reports); + let count = ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, count); + + // but not the admin + let admin_reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(0, admin_reports); + let count = ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?; + assert_eq!(0, count); + + // admin can see the report with `view_mod_reports` set + let admin_reports = ReportCombinedQuery { + show_community_rule_violations: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + assert_length!(1, admin_reports); + + // change a comment to be 3 days old, now admin can also see it by default + update( + report_combined::table.filter(report_combined::dsl::comment_report_id.eq(comment_report.id)), + ) + .set(report_combined::published.eq(Utc::now() - Days::new(3))) + .execute(&mut get_conn(pool).await?) + .await?; + let admin_reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, admin_reports); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn my_reports_only() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports + let sara_report_form = CommentReportForm { + creator_id: data.sara.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from sara".into(), + violates_instance_rules: false, + }; + CommentReport::report(pool, &sara_report_form).await?; + + // timmy reports + let timmy_report_form = CommentReportForm { + creator_id: data.timmy.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from timmy".into(), + violates_instance_rules: false, + }; + CommentReport::report(pool, &timmy_report_form).await?; + + let agg = Comment::read(pool, data.comment.id).await?; + assert_eq!(agg.report_count, 2); + + // Do a batch read of timmys reports, it should only show his own + let reports = ReportCombinedQuery { + my_reports_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + + assert_length!(1, reports); + + if let ReportCombinedView::Comment(v) = &reports[0] { + assert_eq!(v.creator.id, data.timmy.id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } } diff --git a/crates/db_views/src/combined/search_combined_view.rs b/crates/db_views/src/combined/search_combined_view.rs new file mode 100644 index 000000000..0a0ecf4e4 --- /dev/null +++ b/crates/db_views/src/combined/search_combined_view.rs @@ -0,0 +1,1205 @@ +use crate::structs::{ + CommentView, + CommunityView, + LocalUserView, + PersonView, + PostView, + SearchCombinedView, + SearchCombinedViewInternal, +}; +use diesel::{ + dsl::not, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + PgTextExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::{creator_community_actions, creator_local_user}, + impls::{community::community_follower_select_subscribed_type, local_user::local_user_can_mod}, + newtypes::{CommunityId, PaginationCursor, PersonId}, + schema::{ + comment, + comment_actions, + community, + community_actions, + image_details, + local_user, + person, + person_actions, + post, + post_actions, + post_tag, + search_combined, + tag, + }, + source::combined::search::{search_combined_keys as key, SearchCombined}, + traits::{InternalToCombinedView, PaginationCursorBuilder}, + utils::{ + functions::coalesce, + fuzzy_search, + get_conn, + now, + seconds_to_pg_interval, + DbPool, + ReverseTimestampKey, + }, + ListingType, + SearchSortType, + SearchType, +}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use SearchSortType::*; + +impl SearchCombinedViewInternal { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(my_person_id: Option) -> _ { + let item_creator = person::id; + + let item_creator_join = person::table.on( + search_combined::person_id + .eq(item_creator.nullable()) + .or( + search_combined::comment_id + .is_not_null() + .and(comment::creator_id.eq(item_creator)), + ) + .or( + search_combined::post_id + .is_not_null() + .and(post::creator_id.eq(item_creator)), + ) + .and(not(person::deleted)), + ); + + let comment_join = comment::table.on( + search_combined::comment_id + .eq(comment::id.nullable()) + .and(not(comment::removed)) + .and(not(comment::deleted)), + ); + + let post_join = post::table.on( + search_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.eq(post::id)) + .and(not(post::removed)) + .and(not(post::deleted)), + ); + + let community_join = community::table.on( + search_combined::community_id + .eq(community::id.nullable()) + .or(post::community_id.eq(community::id)) + .and(not(community::removed)) + .and(not(community::deleted)), + ); + + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(community::id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(item_creator), + ), + ); + + let local_user_join = local_user::table.on(local_user::person_id.nullable().eq(my_person_id)); + + let creator_local_user_join = creator_local_user.on( + item_creator + .eq(creator_local_user.field(local_user::person_id)) + .and(creator_local_user.field(local_user::admin).eq(true)), + ); + + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(community::id) + .and(community_actions::person_id.nullable().eq(my_person_id)), + ); + + let post_actions_join = post_actions::table.on( + post_actions::post_id + .eq(post::id) + .and(post_actions::person_id.nullable().eq(my_person_id)), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(item_creator) + .and(person_actions::person_id.nullable().eq(my_person_id)), + ); + + let comment_actions_join = comment_actions::table.on( + comment_actions::comment_id + .eq(comment::id) + .and(comment_actions::person_id.nullable().eq(my_person_id)), + ); + + let image_details_join = + image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())); + + search_combined::table + .left_join(comment_join) + .left_join(post_join) + .left_join(item_creator_join) + .left_join(community_join) + .left_join(creator_community_actions_join) + .left_join(local_user_join) + .left_join(creator_local_user_join) + .left_join(community_actions_join) + .left_join(post_actions_join) + .left_join(person_actions_join) + .left_join(comment_actions_join) + .left_join(image_details_join) + } +} + +impl PaginationCursorBuilder for SearchCombinedView { + type CursorData = SearchCombined; + + fn to_cursor(&self) -> PaginationCursor { + let (prefix, id) = match &self { + SearchCombinedView::Post(v) => ('P', v.post.id.0), + SearchCombinedView::Comment(v) => ('C', v.comment.id.0), + SearchCombinedView::Community(v) => ('O', v.community.id.0), + SearchCombinedView::Person(v) => ('E', v.person.id.0), + }; + PaginationCursor::new(prefix, id) + } + + async fn from_cursor( + cursor: &PaginationCursor, + pool: &mut DbPool<'_>, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + let (prefix, id) = cursor.prefix_and_id()?; + + let mut query = search_combined::table + .select(Self::CursorData::as_select()) + .into_boxed(); + + query = match prefix { + 'P' => query.filter(search_combined::post_id.eq(id)), + 'C' => query.filter(search_combined::comment_id.eq(id)), + 'O' => query.filter(search_combined::community_id.eq(id)), + 'E' => query.filter(search_combined::person_id.eq(id)), + _ => return Err(LemmyErrorType::CouldntParsePaginationToken.into()), + }; + let token = query.first(conn).await?; + + Ok(token) + } +} + +#[derive(Default)] +pub struct SearchCombinedQuery { + pub search_term: Option, + pub community_id: Option, + pub creator_id: Option, + pub type_: Option, + pub sort: Option, + pub time_range_seconds: Option, + pub listing_type: Option, + pub title_only: Option, + pub post_url_only: Option, + pub liked_only: Option, + pub disliked_only: Option, + pub cursor_data: Option, + pub page_back: Option, +} + +impl SearchCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &Option, + ) -> LemmyResult> { + let my_person_id = user.as_ref().map(|u| u.local_user.person_id); + let item_creator = person::id; + + let conn = &mut get_conn(pool).await?; + + let post_tags = post_tag::table + .inner_join(tag::table) + .select(diesel::dsl::sql::( + "json_agg(tag.*)", + )) + .filter(post_tag::post_id.eq(post::id)) + .filter(tag::deleted.eq(false)) + .single_value(); + + let mut query = SearchCombinedViewInternal::joins(my_person_id) + .select(( + // Post-specific + post::all_columns.nullable(), + coalesce( + post::comments.nullable() - post_actions::read_comments_amount.nullable(), + post::comments, + ) + .nullable(), + post_actions::saved.nullable(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + post_tags, + // Comment-specific + comment::all_columns.nullable(), + comment_actions::saved.nullable(), + comment_actions::like_score.nullable(), + // Community-specific + community::all_columns.nullable(), + community_actions::blocked.nullable().is_not_null(), + community_follower_select_subscribed_type(), + // // Shared + person::all_columns.nullable(), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + local_user_can_mod(), + )) + .into_boxed(); + + // The filters + + // The search term + if let Some(search_term) = &self.search_term { + if self.post_url_only.unwrap_or_default() { + query = query.filter(post::url.eq(search_term)); + } else { + let searcher = fuzzy_search(search_term); + + let name_or_title_filter = post::name + .ilike(searcher.clone()) + .or(comment::content.ilike(searcher.clone())) + .or(community::name.ilike(searcher.clone())) + .or(community::title.ilike(searcher.clone())) + .or(person::name.ilike(searcher.clone())) + .or(person::display_name.ilike(searcher.clone())); + + let body_or_description_filter = post::body + .ilike(searcher.clone()) + .or(community::description.ilike(searcher.clone())); + + query = if self.title_only.unwrap_or_default() { + query.filter(name_or_title_filter) + } else { + query.filter(name_or_title_filter.or(body_or_description_filter)) + } + } + } + + // Community id + if let Some(community_id) = self.community_id { + query = query.filter(community::id.eq(community_id)); + } + + // Creator id + if let Some(creator_id) = self.creator_id { + query = query.filter(item_creator.eq(creator_id)); + } + + // Liked / disliked filter + if let Some(my_id) = my_person_id { + let not_creator_filter = item_creator.ne(my_id); + let liked_disliked_filter = |score: i16| { + search_combined::post_id + .is_not_null() + .and(post_actions::like_score.eq(score)) + .or( + search_combined::comment_id + .is_not_null() + .and(comment_actions::like_score.eq(score)), + ) + }; + + if self.liked_only.unwrap_or_default() { + query = query + .filter(not_creator_filter) + .filter(liked_disliked_filter(1)); + } else if self.disliked_only.unwrap_or_default() { + query = query + .filter(not_creator_filter) + .filter(liked_disliked_filter(-1)); + } + }; + + // Type + query = match self.type_.unwrap_or_default() { + SearchType::All => query, + SearchType::Posts => query.filter(search_combined::post_id.is_not_null()), + SearchType::Comments => query.filter(search_combined::comment_id.is_not_null()), + SearchType::Communities => query.filter(search_combined::community_id.is_not_null()), + SearchType::Users => query.filter(search_combined::person_id.is_not_null()), + }; + + // Listing type + let is_subscribed = community_actions::followed.is_not_null(); + match self.listing_type.unwrap_or_default() { + ListingType::Subscribed => query = query.filter(is_subscribed), + ListingType::Local => { + query = query.filter( + community::local + .eq(true) + .and(community::hidden.eq(false).or(is_subscribed)) + .or(search_combined::person_id.is_not_null().and(person::local)), + ); + } + ListingType::All => { + query = query.filter( + community::hidden + .eq(false) + .or(is_subscribed) + .or(search_combined::person_id.is_not_null()), + ) + } + ListingType::ModeratorView => { + query = query.filter(community_actions::became_moderator.is_not_null()); + } + } + + // Filter by the time range + if let Some(time_range_seconds) = self.time_range_seconds { + query = query + .filter(search_combined::published.gt(now() - seconds_to_pg_interval(time_range_seconds))); + } + + let mut query = PaginatedQueryBuilder::new(query); + + if self.page_back.unwrap_or_default() { + query = query.before(self.cursor_data).limit_and_offset_from_end(); + } else { + query = query.after(self.cursor_data); + } + + query = match self.sort.unwrap_or_default() { + New => query.then_desc(key::published), + Old => query.then_desc(ReverseTimestampKey(key::published)), + Top => query.then_desc(key::score), + }; + + // finally use unique id as tie breaker + query = query.then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res + .into_iter() + .filter_map(InternalToCombinedView::map_to_enum) + .collect(); + + Ok(out) + } +} + +impl InternalToCombinedView for SearchCombinedViewInternal { + type CombinedView = SearchCombinedView; + + fn map_to_enum(self) -> Option { + // Use for a short alias + let v = self; + + if let (Some(comment), Some(creator), Some(post), Some(community)) = ( + v.comment, + v.item_creator.clone(), + v.post.clone(), + v.community.clone(), + ) { + Some(SearchCombinedView::Comment(CommentView { + comment, + post, + community, + creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + can_mod: v.can_mod, + })) + } else if let (Some(post), Some(creator), Some(community), Some(unread_comments)) = ( + v.post, + v.item_creator.clone(), + v.community.clone(), + v.post_unread_comments, + ) { + Some(SearchCombinedView::Post(PostView { + post, + community, + unread_comments, + creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + image_details: v.image_details, + banned_from_community: v.banned_from_community, + tags: v.post_tags, + can_mod: v.can_mod, + })) + } else if let Some(community) = v.community { + Some(SearchCombinedView::Community(CommunityView { + community, + subscribed: v.subscribed, + blocked: v.community_blocked, + banned_from_community: v.banned_from_community, + can_mod: v.can_mod, + })) + } else if let Some(person) = v.item_creator { + Some(SearchCombinedView::Person(PersonView { + person, + is_admin: v.item_creator_is_admin, + })) + } else { + None + } + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + combined::search_combined_view::SearchCombinedQuery, + structs::{LocalUserView, SearchCombinedView}, + }; + use lemmy_db_schema::{ + assert_length, + source::{ + comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm}, + }, + traits::{Crud, Likeable}, + utils::{build_db_pool_for_tests, DbPool}, + SearchSortType, + SearchType, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + use url::Url; + + struct Data { + instance: Instance, + timmy: Person, + timmy_view: LocalUserView, + sara: Person, + community: Community, + community_2: Community, + timmy_post: Post, + timmy_post_2: Post, + sara_post: Post, + timmy_comment: Comment, + sara_comment: Comment, + sara_comment_2: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); + let sara = Person::create(pool, &sara_form).await?; + + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv"); + let timmy = Person::create(pool, &timmy_form).await?; + let timmy_local_user_form = LocalUserInsertForm::test_form(timmy.id); + let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; + let timmy_view = LocalUserView { + local_user: timmy_local_user, + person: timmy.clone(), + }; + + let community_form = CommunityInsertForm { + description: Some("ask lemmy things".into()), + ..CommunityInsertForm::new( + instance.id, + "asklemmy".to_string(), + "Ask Lemmy".to_owned(), + "pubkey".to_string(), + ) + }; + let community = Community::create(pool, &community_form).await?; + + let community_form_2 = CommunityInsertForm::new( + instance.id, + "startrek_ds9".to_string(), + "Star Trek - Deep Space Nine".to_owned(), + "pubkey".to_string(), + ); + let community_2 = Community::create(pool, &community_form_2).await?; + + let timmy_post_form = PostInsertForm { + body: Some("postbody inside here".into()), + url: Some(Url::parse("https://google.com")?.into()), + ..PostInsertForm::new("timmy post prv".into(), timmy.id, community.id) + }; + let timmy_post = Post::create(pool, &timmy_post_form).await?; + + let timmy_post_form_2 = PostInsertForm::new("timmy post prv 2".into(), timmy.id, community.id); + let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + + let sara_post_form = PostInsertForm::new("sara post prv".into(), sara.id, community_2.id); + let sara_post = Post::create(pool, &sara_post_form).await?; + + let timmy_comment_form = + CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv gold".into()); + let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + + let sara_comment_form = + CommentInsertForm::new(sara.id, sara_post.id, "sara comment prv gold".into()); + let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + + let sara_comment_form_2 = + CommentInsertForm::new(sara.id, timmy_post_2.id, "sara comment prv 2".into()); + let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + + // Timmy likes and dislikes a few things + let timmy_like_post_form = PostLikeForm::new(timmy_post.id, timmy.id, 1); + PostLike::like(pool, &timmy_like_post_form).await?; + + let timmy_like_sara_post_form = PostLikeForm::new(sara_post.id, timmy.id, 1); + PostLike::like(pool, &timmy_like_sara_post_form).await?; + + let timmy_dislike_post_form = PostLikeForm::new(timmy_post_2.id, timmy.id, -1); + PostLike::like(pool, &timmy_dislike_post_form).await?; + + let timmy_like_comment_form = CommentLikeForm { + person_id: timmy.id, + comment_id: timmy_comment.id, + score: 1, + }; + CommentLike::like(pool, &timmy_like_comment_form).await?; + + let timmy_like_sara_comment_form = CommentLikeForm { + person_id: timmy.id, + comment_id: sara_comment.id, + score: 1, + }; + CommentLike::like(pool, &timmy_like_sara_comment_form).await?; + + let timmy_dislike_sara_comment_form = CommentLikeForm { + person_id: timmy.id, + comment_id: sara_comment_2.id, + score: -1, + }; + CommentLike::like(pool, &timmy_dislike_sara_comment_form).await?; + + Ok(Data { + instance, + timmy, + timmy_view, + sara, + community, + community_2, + timmy_post, + timmy_post_2, + sara_post, + timmy_comment, + sara_comment, + sara_comment_2, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // search + let search = SearchCombinedQuery::default().list(pool, &None).await?; + assert_length!(10, search); + + // Make sure the types are correct + if let SearchCombinedView::Comment(v) = &search[0] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Comment(v) = &search[1] { + assert_eq!(data.sara_comment.id, v.comment.id); + assert_eq!(data.sara_post.id, v.post.id); + assert_eq!(data.community_2.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Comment(v) = &search[2] { + assert_eq!(data.timmy_comment.id, v.comment.id); + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Post(v) = &search[3] { + assert_eq!(data.sara_post.id, v.post.id); + assert_eq!(data.community_2.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Post(v) = &search[4] { + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Post(v) = &search[5] { + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Community(v) = &search[6] { + assert_eq!(data.community_2.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Community(v) = &search[7] { + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Person(v) = &search[8] { + assert_eq!(data.timmy.id, v.person.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Person(v) = &search[9] { + assert_eq!(data.sara.id, v.person.id); + } else { + panic!("wrong type"); + } + + // Filtered by community id + let search_by_community = SearchCombinedQuery { + community_id: Some(data.community.id), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(5, search_by_community); + + // Filtered by creator_id + let search_by_creator = SearchCombinedQuery { + creator_id: Some(data.timmy.id), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(4, search_by_creator); + + // Using a term + let search_by_name = SearchCombinedQuery { + search_term: Some("gold".into()), + ..Default::default() + } + .list(pool, &None) + .await?; + + assert_length!(2, search_by_name); + + // Liked / disliked only + let search_liked_only = SearchCombinedQuery { + liked_only: Some(true), + ..Default::default() + } + .list(pool, &Some(data.timmy_view.clone())) + .await?; + + assert_length!(2, search_liked_only); + + let search_disliked_only = SearchCombinedQuery { + disliked_only: Some(true), + ..Default::default() + } + .list(pool, &Some(data.timmy_view.clone())) + .await?; + + assert_length!(1, search_disliked_only); + + // Test sorts + // Test Old sort + let search_old_sort = SearchCombinedQuery { + sort: Some(SearchSortType::Old), + ..Default::default() + } + .list(pool, &Some(data.timmy_view.clone())) + .await?; + if let SearchCombinedView::Person(v) = &search_old_sort[0] { + assert_eq!(data.sara.id, v.person.id); + } else { + panic!("wrong type"); + } + assert_length!(10, search_old_sort); + + // Remove a post and delete a comment + Post::update( + pool, + data.timmy_post_2.id, + &PostUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + + Comment::update( + pool, + data.sara_comment.id, + &CommentUpdateForm { + deleted: Some(true), + ..Default::default() + }, + ) + .await?; + + // 2 things got removed, but the post also has another comment which got removed + let search = SearchCombinedQuery::default().list(pool, &None).await?; + assert_length!(7, search); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn community() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Community search + let community_search = SearchCombinedQuery { + type_: Some(SearchType::Communities), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(2, community_search); + + // Make sure the types are correct + if let SearchCombinedView::Community(v) = &community_search[0] { + assert_eq!(data.community_2.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Community(v) = &community_search[1] { + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + // Filtered by id + let community_search_by_id = SearchCombinedQuery { + community_id: Some(data.community.id), + type_: Some(SearchType::Communities), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(1, community_search_by_id); + + // Using a term + let community_search_by_name = SearchCombinedQuery { + search_term: Some("things".into()), + type_: Some(SearchType::Communities), + ..Default::default() + } + .list(pool, &None) + .await?; + + assert_length!(1, community_search_by_name); + if let SearchCombinedView::Community(v) = &community_search_by_name[0] { + // The asklemmy community + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + // Test title only search to make sure 'ask lemmy things' doesn't get returned + // Using a term + let community_search_title_only = SearchCombinedQuery { + search_term: Some("things".into()), + type_: Some(SearchType::Communities), + title_only: Some(true), + ..Default::default() + } + .list(pool, &None) + .await?; + + assert!(community_search_title_only.is_empty()); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn person() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Person search + let person_search = SearchCombinedQuery { + type_: Some(SearchType::Users), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(2, person_search); + + // Make sure the types are correct + if let SearchCombinedView::Person(v) = &person_search[0] { + assert_eq!(data.timmy.id, v.person.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Person(v) = &person_search[1] { + assert_eq!(data.sara.id, v.person.id); + } else { + panic!("wrong type"); + } + + // Filtered by creator_id + let person_search_by_id = SearchCombinedQuery { + creator_id: Some(data.sara.id), + type_: Some(SearchType::Users), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(1, person_search_by_id); + if let SearchCombinedView::Person(v) = &person_search_by_id[0] { + assert_eq!(data.sara.id, v.person.id); + } else { + panic!("wrong type"); + } + + // Using a term + let person_search_by_name = SearchCombinedQuery { + search_term: Some("tim".into()), + type_: Some(SearchType::Users), + ..Default::default() + } + .list(pool, &None) + .await?; + + assert_length!(1, person_search_by_name); + if let SearchCombinedView::Person(v) = &person_search_by_name[0] { + assert_eq!(data.timmy.id, v.person.id); + } else { + panic!("wrong type"); + } + + // Test Top sorting (uses post score) + let person_search_sort_top = SearchCombinedQuery { + type_: Some(SearchType::Users), + sort: Some(SearchSortType::Top), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(2, person_search_sort_top); + + // Sara should be first, as she has a higher score + if let SearchCombinedView::Person(v) = &person_search_sort_top[0] { + assert_eq!(data.sara.id, v.person.id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn post() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // post search + let post_search = SearchCombinedQuery { + type_: Some(SearchType::Posts), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(3, post_search); + + // Make sure the types are correct + if let SearchCombinedView::Post(v) = &post_search[0] { + assert_eq!(data.sara_post.id, v.post.id); + assert_eq!(data.community_2.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Post(v) = &post_search[1] { + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Post(v) = &post_search[2] { + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + // Filtered by id + let post_search_by_community = SearchCombinedQuery { + community_id: Some(data.community.id), + type_: Some(SearchType::Posts), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(2, post_search_by_community); + + // Using a term + let post_search_by_name = SearchCombinedQuery { + search_term: Some("sara".into()), + type_: Some(SearchType::Posts), + ..Default::default() + } + .list(pool, &None) + .await?; + + assert_length!(1, post_search_by_name); + + // Test title only search to make sure 'postbody' doesn't show up + // Using a term + let post_search_title_only = SearchCombinedQuery { + search_term: Some("postbody".into()), + type_: Some(SearchType::Posts), + title_only: Some(true), + ..Default::default() + } + .list(pool, &None) + .await?; + + assert!(post_search_title_only.is_empty()); + + // Test title only search to make sure 'postbody' doesn't show up + // Using a term + let post_search_url_only = SearchCombinedQuery { + search_term: data.timmy_post.url.as_ref().map(ToString::to_string), + type_: Some(SearchType::Posts), + post_url_only: Some(true), + ..Default::default() + } + .list(pool, &None) + .await?; + + assert_length!(1, post_search_url_only); + + // Liked / disliked only + let post_search_liked_only = SearchCombinedQuery { + type_: Some(SearchType::Posts), + liked_only: Some(true), + ..Default::default() + } + .list(pool, &Some(data.timmy_view.clone())) + .await?; + + // Should only be 1 not 2, because liked only ignores your own content + assert_length!(1, post_search_liked_only); + + let post_search_disliked_only = SearchCombinedQuery { + type_: Some(SearchType::Posts), + disliked_only: Some(true), + ..Default::default() + } + .list(pool, &Some(data.timmy_view.clone())) + .await?; + + // Should be zero because you disliked your own post + assert_length!(0, post_search_disliked_only); + + // Test top sort + let post_search_sort_top = SearchCombinedQuery { + type_: Some(SearchType::Posts), + sort: Some(SearchSortType::Top), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(3, post_search_sort_top); + + // Timmy_post_2 has a dislike, so it should be last + if let SearchCombinedView::Post(v) = &post_search_sort_top[2] { + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn comment() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // comment search + let comment_search = SearchCombinedQuery { + type_: Some(SearchType::Comments), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(3, comment_search); + + // Make sure the types are correct + if let SearchCombinedView::Comment(v) = &comment_search[0] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Comment(v) = &comment_search[1] { + assert_eq!(data.sara_comment.id, v.comment.id); + assert_eq!(data.sara_post.id, v.post.id); + assert_eq!(data.community_2.id, v.community.id); + } else { + panic!("wrong type"); + } + + if let SearchCombinedView::Comment(v) = &comment_search[2] { + assert_eq!(data.timmy_comment.id, v.comment.id); + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + // Filtered by id + let comment_search_by_community = SearchCombinedQuery { + community_id: Some(data.community.id), + type_: Some(SearchType::Comments), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(2, comment_search_by_community); + + // Using a term + let comment_search_by_name = SearchCombinedQuery { + search_term: Some("gold".into()), + type_: Some(SearchType::Comments), + ..Default::default() + } + .list(pool, &None) + .await?; + + assert_length!(2, comment_search_by_name); + + // Liked / disliked only + let comment_search_liked_only = SearchCombinedQuery { + type_: Some(SearchType::Comments), + liked_only: Some(true), + ..Default::default() + } + .list(pool, &Some(data.timmy_view.clone())) + .await?; + + assert_length!(1, comment_search_liked_only); + + let comment_search_disliked_only = SearchCombinedQuery { + type_: Some(SearchType::Comments), + disliked_only: Some(true), + ..Default::default() + } + .list(pool, &Some(data.timmy_view.clone())) + .await?; + + assert_length!(1, comment_search_disliked_only); + + // Test top sort + let comment_search_sort_top = SearchCombinedQuery { + type_: Some(SearchType::Comments), + sort: Some(SearchSortType::Top), + ..Default::default() + } + .list(pool, &None) + .await?; + assert_length!(3, comment_search_sort_top); + + // Sara comment 2 is disliked, so should be last + if let SearchCombinedView::Comment(v) = &comment_search_sort_top[2] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment/comment_view.rs similarity index 83% rename from crates/db_views/src/comment_view.rs rename to crates/db_views/src/comment/comment_view.rs index 710a820e7..3bdb5a108 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment/comment_view.rs @@ -1,25 +1,26 @@ -use crate::structs::CommentView; +use crate::{ + structs::{CommentSlimView, CommentView}, + utils::filter_blocked, +}; use diesel::{ - dsl::{exists, not}, - pg::Pg, + dsl::exists, result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, - PgTextExpressionMethods, QueryDsl, + SelectableHelper, }; 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}, + newtypes::{CommentId, CommunityId, PersonId, PostId}, schema::{ comment, comment_actions, - comment_aggregates, community, community_actions, instance_actions, @@ -29,102 +30,79 @@ use lemmy_db_schema::{ person_actions, post, }, - source::{ - community::{CommunityFollower, CommunityFollowerState}, - local_user::LocalUser, - site::Site, - }, - utils::{ - actions, - actions_alias, - fuzzy_search, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + source::{community::CommunityFollowerState, local_user::LocalUser, site::Site}, + utils::{get_conn, limit_and_offset, now, seconds_to_pg_interval, DbPool}, CommentSortType, CommunityVisibility, ListingType, }; -type QueriesReadTypes<'a> = (CommentId, Option<&'a LocalUser>); -type QueriesListTypes<'a> = (CommentQuery<'a>, &'a Site); +impl CommentView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(my_person_id: Option) -> _ { + let community_join = community::table.on(post::community_id.eq(community::id)); -fn queries<'a>() -> Queries< - impl ReadFn<'a, CommentView, QueriesReadTypes<'a>>, - impl ListFn<'a, CommentView, QueriesListTypes<'a>>, -> { - let creator_is_admin = exists( - local_user::table.filter( - comment::creator_id - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ); + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(post::community_id) + .and(community_actions::person_id.nullable().eq(my_person_id)), + ); - let all_joins = move |query: comment::BoxedQuery<'a, Pg>, my_person_id: Option| { - query + let comment_actions_join = comment_actions::table.on( + comment_actions::comment_id + .eq(comment::id) + .and(comment_actions::person_id.nullable().eq(my_person_id)), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(comment::creator_id) + .and(person_actions::person_id.nullable().eq(my_person_id)), + ); + + let instance_actions_join = instance_actions::table.on( + instance_actions::instance_id + .eq(community::instance_id) + .and(instance_actions::person_id.nullable().eq(my_person_id)), + ); + + let comment_creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(comment::creator_id), + ), + ); + + let local_user_join = local_user::table.on(local_user::person_id.nullable().eq(my_person_id)); + + comment::table .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(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, - 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, - CommunityFollower::select_subscribed_type(), - comment_actions::saved.nullable().is_not_null(), - person_actions::blocked.nullable().is_not_null(), - comment_actions::like_score.nullable(), - )) - }; + .inner_join(community_join) + .left_join(community_actions_join) + .left_join(comment_actions_join) + .left_join(person_actions_join) + .left_join(instance_actions_join) + .left_join(comment_creator_community_actions_join) + .left_join(local_user_join) + } + + pub async fn read( + pool: &mut DbPool<'_>, + comment_id: CommentId, + my_local_user: Option<&'_ LocalUser>, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + let mut query = Self::joins(my_local_user.person_id()) + .filter(comment::id.eq(comment_id)) + .select(Self::as_select()) + .into_boxed(); - let read = move |mut conn: DbConn<'a>, - (comment_id, my_local_user): (CommentId, Option<&'a LocalUser>)| async move { - let mut query = all_joins( - comment::table.find(comment_id).into_boxed(), - my_local_user.person_id(), - ); query = my_local_user.visible_communities_only(query); // Check permissions to view private community content. @@ -138,14 +116,64 @@ fn queries<'a>() -> Queries< .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } - query.first(&mut conn).await - }; - let list = move |mut conn: DbConn<'a>, (o, site): (CommentQuery<'a>, &'a Site)| async move { + let mut res = query.first::(conn).await?; + + // If a person is given, then my_vote (res.9), if None, should be 0, not null + // Necessary to differentiate between other person's votes + if my_local_user.is_some() && res.my_vote.is_none() { + res.my_vote = Some(0); + } + + Ok(res) + } + + pub fn map_to_slim(self) -> CommentSlimView { + CommentSlimView { + comment: self.comment, + creator: self.creator, + creator_banned_from_community: self.creator_banned_from_community, + banned_from_community: self.banned_from_community, + creator_is_moderator: self.creator_is_moderator, + creator_is_admin: self.creator_is_admin, + subscribed: self.subscribed, + saved: self.saved, + creator_blocked: self.creator_blocked, + my_vote: self.my_vote, + can_mod: self.can_mod, + } + } +} + +#[derive(Default)] +pub struct CommentQuery<'a> { + pub listing_type: Option, + pub sort: Option, + pub time_range_seconds: Option, + pub community_id: Option, + pub post_id: Option, + pub parent_path: Option, + pub creator_id: Option, + pub local_user: Option<&'a LocalUser>, + pub liked_only: Option, + pub disliked_only: Option, + pub page: Option, + pub limit: Option, + pub max_depth: Option, +} + +impl CommentQuery<'_> { + pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let o = self; + // The left join below will return None in this case - let local_user_id_join = o.local_user.local_user_id().unwrap_or(LocalUserId(-1)); + let my_person_id = o.local_user.person_id(); + let local_user_id = o.local_user.local_user_id(); - let mut query = all_joins(comment::table.into_boxed(), o.local_user.person_id()); + let mut query = CommentView::joins(my_person_id) + .select(CommentView::as_select()) + .into_boxed(); if let Some(creator_id) = o.creator_id { query = query.filter(comment::creator_id.eq(creator_id)); @@ -158,14 +186,6 @@ fn queries<'a>() -> Queries< if let Some(parent_path) = o.parent_path.as_ref() { query = query.filter(comment::path.contained_by(parent_path)); }; - //filtering out removed and deleted comments from search - if let Some(search_term) = o.search_term { - query = query.filter( - comment::content - .ilike(fuzzy_search(&search_term)) - .and(not(comment::removed.or(comment::deleted))), - ); - }; if let Some(community_id) = o.community_id { query = query.filter(post::community_id.eq(community_id)); @@ -173,20 +193,16 @@ fn queries<'a>() -> Queries< let is_subscribed = community_actions::followed.is_not_null(); - match o.listing_type.unwrap_or_default() { - ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ - ListingType::Local => { - query = query - .filter(community::local.eq(true)) - .filter(community::hidden.eq(false).or(is_subscribed)) - } - ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), - ListingType::ModeratorView => { - query = query.filter(community_actions::became_moderator.is_not_null()); - } - } + // For posts, we only show hidden if its subscribed, but for comments, + // we ignore hidden. + query = match o.listing_type.unwrap_or_default() { + ListingType::Subscribed => query.filter(is_subscribed), + ListingType::Local => query.filter(community::local.eq(true)), + ListingType::All => query, + ListingType::ModeratorView => query.filter(community_actions::became_moderator.is_not_null()), + }; - if let Some(my_id) = o.local_user.person_id() { + if let Some(my_id) = my_person_id { let not_creator_filter = comment::creator_id.ne(my_id); if o.liked_only.unwrap_or_default() { query = query @@ -209,15 +225,15 @@ fn queries<'a>() -> Queries< local_user_language::table.filter( comment::language_id .eq(local_user_language::language_id) - .and(local_user_language::local_user_id.eq(local_user_id_join)), + .and( + local_user_language::local_user_id + .nullable() + .eq(local_user_id), + ), ), )); - // Don't show blocked communities or persons - query = query - .filter(instance_actions::blocked.is_null()) - .filter(community_actions::blocked.is_null()) - .filter(person_actions::blocked.is_null()); + query = query.filter(filter_blocked()); }; if !o.local_user.show_nsfw(site) { @@ -277,95 +293,42 @@ fn queries<'a>() -> Queries< query = match o.sort.unwrap_or(CommentSortType::Hot) { CommentSortType::Hot => query - .then_order_by(comment_aggregates::hot_rank.desc()) - .then_order_by(comment_aggregates::score.desc()), - CommentSortType::Controversial => { - query.then_order_by(comment_aggregates::controversy_rank.desc()) - } + .then_order_by(comment::hot_rank.desc()) + .then_order_by(comment::score.desc()), + CommentSortType::Controversial => query.then_order_by(comment::controversy_rank.desc()), CommentSortType::New => query.then_order_by(comment::published.desc()), CommentSortType::Old => query.then_order_by(comment::published.asc()), - CommentSortType::Top => query.then_order_by(comment_aggregates::score.desc()), + CommentSortType::Top => query.then_order_by(comment::score.desc()), }; - // Note: deleted and removed comments are done on the front side - query + // Filter by the time range + if let Some(time_range_seconds) = o.time_range_seconds { + query = + query.filter(comment::published.gt(now() - seconds_to_pg_interval(time_range_seconds))); + } + + let res = query .limit(limit) .offset(offset) - .load::(&mut conn) - .await - }; + .load::(conn) + .await?; - Queries::new(read, list) -} - -impl CommentView { - pub async fn read( - pool: &mut DbPool<'_>, - comment_id: CommentId, - my_local_user: Option<&'_ LocalUser>, - ) -> Result { - let is_admin = my_local_user.map(|u| u.admin).unwrap_or(false); - // If a person is given, then my_vote (res.9), if None, should be 0, not null - // Necessary to differentiate between other person's votes - let mut res = queries().read(pool, (comment_id, my_local_user)).await?; - if my_local_user.is_some() && res.my_vote.is_none() { - res.my_vote = Some(0); - } - Ok(handle_deleted(res, is_admin)) + Ok(res) } } -#[derive(Default)] -pub struct CommentQuery<'a> { - pub listing_type: Option, - pub sort: Option, - pub community_id: Option, - pub post_id: Option, - pub parent_path: Option, - pub creator_id: Option, - pub local_user: Option<&'a LocalUser>, - pub search_term: Option, - pub liked_only: Option, - pub disliked_only: Option, - pub page: Option, - pub limit: Option, - pub max_depth: Option, -} - -impl CommentQuery<'_> { - pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { - let is_admin = self.local_user.map(|u| u.admin).unwrap_or(false); - Ok( - queries() - .list(pool, (self, site)) - .await? - .into_iter() - .map(|c| handle_deleted(c, is_admin)) - .collect(), - ) - } -} - -fn handle_deleted(mut c: CommentView, is_admin: bool) -> CommentView { - if !is_admin && (c.comment.deleted || c.comment.removed) { - c.comment.content = String::new(); - } - c -} - #[cfg(test)] #[expect(clippy::indexing_slicing)] mod tests { use crate::{ - comment_view::{CommentQuery, CommentSortType, CommentView, DbPool}, + comment::comment_view::{CommentQuery, CommentSortType, CommentView, DbPool}, structs::LocalUserView, }; use lemmy_db_schema::{ - aggregates::structs::CommentAggregates, assert_length, impls::actor_language::UNDETERMINED_ID, - newtypes::LanguageId, + newtypes::{CommentId, LanguageId}, source::{ actor_language::LocalUserLanguage, comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, @@ -383,8 +346,7 @@ mod tests { }, instance::Instance, language::Language, - local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, + local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, person::{Person, PersonInsertForm}, person_block::{PersonBlock, PersonBlockForm}, post::{Post, PostInsertForm, PostUpdateForm}, @@ -404,6 +366,7 @@ mod tests { inserted_comment_0: Comment, inserted_comment_1: Comment, inserted_comment_2: Comment, + inserted_comment_5: Comment, inserted_post: Post, timmy_local_user_view: LocalUserView, inserted_sara_person: Person, @@ -511,7 +474,7 @@ mod tests { inserted_post.id, "Comment 5".into(), ); - let _inserted_comment_5 = + let inserted_comment_5 = Comment::create(pool, &comment_form_5, Some(&inserted_comment_4.path)).await?; let timmy_blocks_sara_form = PersonBlockForm { @@ -538,9 +501,7 @@ mod tests { let timmy_local_user_view = LocalUserView { local_user: inserted_timmy_local_user.clone(), - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), person: inserted_timmy_person.clone(), - counts: Default::default(), }; let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id); let site = Site::create(pool, &site_form).await?; @@ -549,6 +510,7 @@ mod tests { inserted_comment_0, inserted_comment_1, inserted_comment_2, + inserted_comment_5, inserted_post, timmy_local_user_view, inserted_sara_person, @@ -564,10 +526,11 @@ mod tests { let pool = &mut pool.into(); let data = init_data(pool).await?; - let expected_comment_view_no_person = expected_comment_view(&data, pool).await?; + let expected_comment_view_no_person = expected_comment_view(&data); let mut expected_comment_view_with_person = expected_comment_view_no_person.clone(); expected_comment_view_with_person.my_vote = Some(1); + expected_comment_view_with_person.can_mod = true; let read_comment_views_no_person = CommentQuery { sort: (Some(CommentSortType::Old)), @@ -695,10 +658,10 @@ mod tests { // Make sure it contains the parent, but not the comment from the other tree let child_comments = read_comment_views_child_path .into_iter() - .map(|c| c.comment) - .collect::>(); - assert!(child_comments.contains(&data.inserted_comment_1)); - assert!(!child_comments.contains(&data.inserted_comment_2)); + .map(|c| c.comment.id) + .collect::>(); + assert!(child_comments.contains(&data.inserted_comment_1.id)); + assert!(!child_comments.contains(&data.inserted_comment_2.id)); let read_comment_views_top_max_depth = CommentQuery { post_id: (Some(data.inserted_post.id)), @@ -710,7 +673,7 @@ mod tests { // Make sure a depth limited one only has the top comment assert_eq!( - expected_comment_view(&data, pool).await?, + expected_comment_view(&data), read_comment_views_top_max_depth[0] ); assert_length!(1, read_comment_views_top_max_depth); @@ -767,7 +730,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_length!(2, finnish_comments); + assert_length!(1, finnish_comments); let finnish_comment = finnish_comments .iter() .find(|c| c.comment.language_id == finnish_id); @@ -896,17 +859,17 @@ mod tests { Ok(()) } - async fn expected_comment_view(data: &Data, pool: &mut DbPool<'_>) -> LemmyResult { - let agg = CommentAggregates::read(pool, data.inserted_comment_0.id).await?; - Ok(CommentView { + fn expected_comment_view(data: &Data) -> CommentView { + CommentView { creator_banned_from_community: false, banned_from_community: false, creator_is_moderator: false, creator_is_admin: true, my_vote: None, subscribed: SubscribedType::NotSubscribed, - saved: false, + saved: None, creator_blocked: false, + can_mod: false, comment: Comment { id: data.inserted_comment_0.id, content: "Comment 0".into(), @@ -921,6 +884,14 @@ mod tests { distinguished: false, path: data.inserted_comment_0.clone().path, language_id: LanguageId(37), + score: 1, + upvotes: 1, + downvotes: 0, + child_count: 5, + hot_rank: RANK_DEFAULT, + controversy_rank: 0.0, + report_count: 0, + unresolved_report_count: 0, }, creator: Person { id: data.timmy_local_user_view.person.id, @@ -928,7 +899,7 @@ mod tests { display_name: None, published: data.timmy_local_user_view.person.published, avatar: None, - actor_id: data.timmy_local_user_view.person.actor_id.clone(), + ap_id: data.timmy_local_user_view.person.ap_id.clone(), local: true, banned: false, deleted: false, @@ -943,6 +914,10 @@ mod tests { private_key: data.timmy_local_user_view.person.private_key.clone(), public_key: data.timmy_local_user_view.person.public_key.clone(), last_refreshed_at: data.timmy_local_user_view.person.last_refreshed_at, + post_count: 1, + post_score: 0, + comment_count: 5, + comment_score: 1, }, post: Post { id: data.inserted_post.id, @@ -969,6 +944,19 @@ mod tests { featured_local: false, url_content_type: None, scheduled_publish_time: None, + comments: 6, + score: 0, + upvotes: 0, + downvotes: 0, + newest_comment_time_necro: data.inserted_comment_1.published, + newest_comment_time: data.inserted_comment_5.published, + hot_rank: RANK_DEFAULT, + hot_rank_active: RANK_DEFAULT, + controversy_rank: 0.0, + scaled_rank: RANK_DEFAULT, + instance_id: data.inserted_instance.id, + report_count: 0, + unresolved_report_count: 0, }, community: Community { id: data.inserted_community.id, @@ -977,7 +965,7 @@ mod tests { removed: false, deleted: false, nsfw: false, - actor_id: data.inserted_community.actor_id.clone(), + ap_id: data.inserted_community.ap_id.clone(), local: true, title: "nada".to_owned(), sidebar: None, @@ -997,20 +985,20 @@ mod tests { featured_url: data.inserted_community.featured_url.clone(), visibility: CommunityVisibility::Public, random_number: data.inserted_community.random_number, - }, - counts: CommentAggregates { - comment_id: data.inserted_comment_0.id, - score: 1, - upvotes: 1, - downvotes: 0, - published: agg.published, - child_count: 5, + subscribers: 0, + posts: 1, + comments: 6, + users_active_day: 0, + users_active_week: 0, + users_active_month: 0, + users_active_half_year: 0, hot_rank: RANK_DEFAULT, - controversy_rank: 0.0, + subscribers_local: 0, report_count: 0, unresolved_report_count: 0, + interactions_month: 0, }, - }) + } } #[tokio::test] @@ -1256,6 +1244,16 @@ mod tests { Comment::update(pool, data.inserted_comment_0.id, &form).await?; // Read as normal user, content is cleared + // Timmy leaves admin + LocalUser::update( + pool, + data.timmy_local_user_view.local_user.id, + &LocalUserUpdateForm { + admin: Some(false), + ..Default::default() + }, + ) + .await?; data.timmy_local_user_view.local_user.admin = false; let comment_view = CommentView::read( pool, @@ -1275,6 +1273,15 @@ mod tests { assert_eq!("", comment_listing[0].comment.content); // Read as admin, content is returned + LocalUser::update( + pool, + data.timmy_local_user_view.local_user.id, + &LocalUserUpdateForm { + admin: Some(true), + ..Default::default() + }, + ) + .await?; data.timmy_local_user_view.local_user.admin = true; let comment_view = CommentView::read( pool, diff --git a/crates/db_views/src/comment/mod.rs b/crates/db_views/src/comment/mod.rs new file mode 100644 index 000000000..b6f4b0b75 --- /dev/null +++ b/crates/db_views/src/comment/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "full")] +pub mod comment_view; diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs deleted file mode 100644 index 6154b9b56..000000000 --- a/crates/db_views/src/comment_report_view.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::structs::CommentReportView; -use diesel::{ - dsl::now, - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - aliases::{self, creator_community_actions}, - newtypes::{CommentReportId, PersonId}, - schema::{ - comment, - comment_actions, - comment_aggregates, - comment_report, - community, - community_actions, - local_user, - person, - person_actions, - post, - }, - source::community::CommunityFollower, - utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, -}; - -impl CommentReportView { - /// returns the CommentReportView for the provided report_id - /// - /// * `report_id` - the report id to obtain - pub async fn read( - pool: &mut DbPool<'_>, - report_id: CommentReportId, - my_person_id: PersonId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - comment_report::table - .find(report_id) - .inner_join(comment::table) - .inner_join(post::table.on(comment::post_id.eq(post::id))) - .inner_join(community::table.on(post::community_id.eq(community::id))) - .inner_join(person::table.on(comment_report::creator_id.eq(person::id))) - .inner_join(aliases::person1.on(comment::creator_id.eq(aliases::person1.field(person::id)))) - .inner_join( - comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_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(actions_alias( - creator_community_actions, - comment::creator_id, - post::community_id, - )) - .left_join( - local_user::table.on( - comment::creator_id - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ) - .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, - post::all_columns, - community::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - comment_aggregates::all_columns, - 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_actions::blocked.nullable().is_not_null(), - CommunityFollower::select_subscribed_type(), - comment_actions::saved.nullable().is_not_null(), - comment_actions::like_score.nullable(), - aliases::person2.fields(person::all_columns).nullable(), - )) - .first(conn) - .await - } -} diff --git a/crates/db_views_actor/src/community_follower_view.rs b/crates/db_views/src/community/community_follower_view.rs similarity index 89% rename from crates/db_views_actor/src/community_follower_view.rs rename to crates/db_views/src/community/community_follower_view.rs index c32ccb5b8..60ec9d960 100644 --- a/crates/db_views_actor/src/community_follower_view.rs +++ b/crates/db_views/src/community/community_follower_view.rs @@ -8,22 +8,31 @@ use diesel::{ ExpressionMethods, JoinOnDsl, QueryDsl, + SelectableHelper, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ + impls::community::community_follower_select_subscribed_type, newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, schema::{community, community_actions, person}, source::{ - community::{Community, CommunityFollower, CommunityFollowerState}, + community::{Community, CommunityFollowerState}, person::Person, }, - utils::{action_query, get_conn, limit_and_offset, DbPool}, + utils::{get_conn, limit_and_offset, DbPool}, CommunityVisibility, SubscribedType, }; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; impl CommunityFollowerView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins() -> _ { + community_actions::table + .filter(community_actions::followed.is_not_null()) + .inner_join(community::table) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + } /// return a list of local community ids and remote inboxes that at least one user of the given /// instance has followed pub async fn get_instance_followed_community_inboxes( @@ -39,9 +48,7 @@ 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_actions::table - .inner_join(community::table) - .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + Self::joins() .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 @@ -53,15 +60,15 @@ impl CommunityFollowerView { .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 = action_query(community_actions::followed) + let res = Self::joins() .filter(community_actions::community_id.eq(community_id)) .filter(not(person::local)) - .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .select(person::inbox_url) .distinct() .load::(conn) @@ -69,12 +76,13 @@ impl CommunityFollowerView { Ok(res) } + pub async fn count_community_followers( pool: &mut DbPool<'_>, community_id: CommunityId, ) -> Result { let conn = &mut get_conn(pool).await?; - let res = action_query(community_actions::followed) + let res = Self::joins() .filter(community_actions::community_id.eq(community_id)) .select(count_star()) .first::(conn) @@ -85,13 +93,11 @@ impl CommunityFollowerView { pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { let conn = &mut get_conn(pool).await?; - action_query(community_actions::followed) - .inner_join(community::table) - .inner_join(person::table.on(community_actions::person_id.eq(person::id))) - .select((community::all_columns, person::all_columns)) + Self::joins() .filter(community_actions::person_id.eq(person_id)) .filter(community::deleted.eq(false)) .filter(community::removed.eq(false)) + .select(Self::as_select()) .order_by(community::title) .load::(conn) .await @@ -140,9 +146,13 @@ impl CommunityFollowerView { ), )); - let mut query = action_query(community_actions::followed) - .inner_join(person::table.on(community_actions::person_id.eq(person::id))) - .inner_join(community::table) + let mut query = Self::joins() + .select(( + person::all_columns, + community::all_columns, + is_new_instance, + community_follower_select_subscribed_type(), + )) .into_boxed(); if all_communities { // if param is false, only return items for communities where user is a mod @@ -158,12 +168,6 @@ impl CommunityFollowerView { .order_by(community_actions::followed.asc()) .limit(limit) .offset(offset) - .select(( - person::all_columns, - community::all_columns, - is_new_instance, - CommunityFollower::select_subscribed_type(), - )) .load::<(Person, Community, bool, SubscribedType)>(conn) .await?; Ok( @@ -186,8 +190,7 @@ impl CommunityFollowerView { community_id: CommunityId, ) -> Result { let conn = &mut get_conn(pool).await?; - action_query(community_actions::followed) - .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + Self::joins() .filter(community_actions::community_id.eq(community_id)) .filter(community_actions::follow_state.eq(CommunityFollowerState::ApprovalRequired)) .select(count(community_actions::community_id)) @@ -204,7 +207,7 @@ impl CommunityFollowerView { } let conn = &mut get_conn(pool).await?; select(exists( - action_query(community_actions::followed) + Self::joins() .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)), @@ -221,8 +224,7 @@ impl CommunityFollowerView { ) -> 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))) + Self::joins() .filter(community_actions::community_id.eq(community_id)) .filter(person::instance_id.eq(instance_id)) .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), @@ -240,8 +242,7 @@ impl CommunityFollowerView { ) -> 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))) + Self::joins() .filter(community_actions::community_id.eq(community_id)) .filter(person::instance_id.eq(instance_id)) .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), diff --git a/crates/db_views_actor/src/community_moderator_view.rs b/crates/db_views/src/community/community_moderator_view.rs similarity index 67% rename from crates/db_views_actor/src/community_moderator_view.rs rename to crates/db_views/src/community/community_moderator_view.rs index a9ada92e1..ad60675aa 100644 --- a/crates/db_views_actor/src/community_moderator_view.rs +++ b/crates/db_views/src/community/community_moderator_view.rs @@ -1,26 +1,43 @@ use crate::structs::CommunityModeratorView; -use diesel::{dsl::exists, result::Error, select, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{ + dsl::exists, + result::Error, + select, + ExpressionMethods, + JoinOnDsl, + QueryDsl, + SelectableHelper, +}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, schema::{community, community_actions, person}, source::local_user::LocalUser, - utils::{action_query, find_action, get_conn, DbPool}, + utils::{get_conn, DbPool}, }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CommunityModeratorView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins() -> _ { + community_actions::table + .filter(community_actions::became_moderator.is_not_null()) + .inner_join(community::table) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + } + pub async fn check_is_community_moderator( pool: &mut DbPool<'_>, - find_community_id: CommunityId, - find_person_id: PersonId, + community_id: CommunityId, + person_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(exists(find_action( - community_actions::became_moderator, - (find_person_id, find_community_id), - ))) + select(exists( + Self::joins() + .filter(community_actions::person_id.eq(person_id)) + .filter(community_actions::community_id.eq(community_id)), + )) .get_result::(conn) .await? .then_some(()) @@ -29,12 +46,11 @@ impl CommunityModeratorView { pub(crate) async fn is_community_moderator_of_any( pool: &mut DbPool<'_>, - find_person_id: PersonId, + person_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; select(exists( - action_query(community_actions::became_moderator) - .filter(community_actions::person_id.eq(find_person_id)), + Self::joins().filter(community_actions::person_id.eq(person_id)), )) .get_result::(conn) .await? @@ -47,13 +63,11 @@ impl CommunityModeratorView { community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - action_query(community_actions::became_moderator) - .inner_join(community::table) - .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + Self::joins() .filter(community_actions::community_id.eq(community_id)) - .select((community::all_columns, person::all_columns)) + .select(Self::as_select()) .order_by(community_actions::became_moderator) - .load::(conn) + .load::(conn) .await } @@ -63,11 +77,9 @@ impl CommunityModeratorView { local_user: Option<&LocalUser>, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let mut query = action_query(community_actions::became_moderator) - .inner_join(community::table) - .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + let mut query = Self::joins() .filter(community_actions::person_id.eq(person_id)) - .select((community::all_columns, person::all_columns)) + .select(Self::as_select()) .into_boxed(); query = local_user.visible_communities_only(query); @@ -82,17 +94,15 @@ impl CommunityModeratorView { query = query.filter(community::removed.eq(false)) } - query.load::(conn).await + query.load::(conn).await } /// Finds all communities first mods / creators /// 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?; - action_query(community_actions::became_moderator) - .inner_join(community::table) - .inner_join(person::table.on(person::id.eq(community_actions::person_id))) - .select((community::all_columns, person::all_columns)) + Self::joins() + .select(Self::as_select()) // 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_actions::community_id) @@ -100,7 +110,7 @@ impl CommunityModeratorView { community_actions::community_id, community_actions::became_moderator, )) - .load::(conn) + .load::(conn) .await } } diff --git a/crates/db_views_actor/src/community_person_ban_view.rs b/crates/db_views/src/community/community_person_ban_view.rs similarity index 55% rename from crates/db_views_actor/src/community_person_ban_view.rs rename to crates/db_views/src/community/community_person_ban_view.rs index 224ea8d53..4c89046dc 100644 --- a/crates/db_views_actor/src/community_person_ban_view.rs +++ b/crates/db_views/src/community/community_person_ban_view.rs @@ -2,12 +2,14 @@ use crate::structs::CommunityPersonBanView; use diesel::{ dsl::{exists, not}, select, + ExpressionMethods, + QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, PersonId}, schema::community_actions, - utils::{find_action, get_conn, DbPool}, + utils::{get_conn, DbPool}, }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -18,13 +20,13 @@ impl CommunityPersonBanView { from_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists(find_action( - community_actions::received_ban, - (from_person_id, from_community_id), - )))) - .get_result::(conn) - .await? - .then_some(()) - .ok_or(LemmyErrorType::PersonIsBannedFromCommunity.into()) + let find_action = community_actions::table + .find((from_person_id, from_community_id)) + .filter(community_actions::received_ban.is_not_null()); + select(not(exists(find_action))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::PersonIsBannedFromCommunity.into()) } } diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views/src/community/community_view.rs similarity index 67% rename from crates/db_views_actor/src/community_view.rs rename to crates/db_views/src/community/community_view.rs index 1a8e3c4cd..e7245acd7 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views/src/community/community_view.rs @@ -1,183 +1,71 @@ use crate::structs::{CommunityModeratorView, CommunitySortType, CommunityView, PersonView}; use diesel::{ - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, + JoinOnDsl, NullableExpressionMethods, - PgTextExpressionMethods, QueryDsl, + SelectableHelper, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, - schema::{community, community_actions, community_aggregates, instance_actions}, + schema::{community, community_actions, instance_actions, local_user}, source::{ - community::{CommunityFollower, CommunityFollowerState}, + community::{Community, CommunityFollowerState}, local_user::LocalUser, site::Site, }, - utils::{ - actions, - functions::lower, - fuzzy_search, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{functions::lower, get_conn, limit_and_offset, now, seconds_to_pg_interval, DbPool}, ListingType, - PostSortType, }; 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, QueriesReadTypes<'a>>, - impl ListFn<'a, CommunityView, QueriesListTypes<'a>>, -> { - let all_joins = |query: community::BoxedQuery<'a, Pg>, my_local_user: Option<&'a LocalUser>| { - query - .inner_join(community_aggregates::table) - .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_actions::blocked.nullable().is_not_null(), - community_aggregates::all_columns, - community_actions::received_ban.nullable().is_not_null(), - ); - - let not_removed_or_deleted = community::removed - .eq(false) - .and(community::deleted.eq(false)); - - let read = move |mut conn: DbConn<'a>, - (community_id, my_local_user, is_mod_or_admin): ( - CommunityId, - Option<&'a LocalUser>, - bool, - )| async move { - let mut query = all_joins( - community::table.find(community_id).into_boxed(), - my_local_user, - ) - .select(selection); - - // Hide deleted and removed for non-admins or mods - if !is_mod_or_admin { - query = query.filter(not_removed_or_deleted); - } - - query = my_local_user.visible_communities_only(query); - - query.first(&mut conn).await - }; - - let list = move |mut conn: DbConn<'a>, (o, site): (CommunityQuery<'a>, &'a Site)| async move { - use CommunitySortType::*; - - let mut query = all_joins(community::table.into_boxed(), o.local_user).select(selection); - - if let Some(search_term) = o.search_term { - let searcher = fuzzy_search(&search_term); - let name_filter = community::name.ilike(searcher.clone()); - let title_filter = community::title.ilike(searcher.clone()); - let description_filter = community::description.ilike(searcher.clone()); - query = if o.title_only.unwrap_or_default() { - query.filter(name_filter.or(title_filter)) - } else { - query.filter(name_filter.or(title_filter.or(description_filter))) - } - } - - // Hide deleted and removed for non-admins or mods - if !o.is_mod_or_admin { - query = query.filter(not_removed_or_deleted).filter( - community::hidden - .eq(false) - .or(community_actions::follow_state.is_not_null()), - ); - } - - match o.sort.unwrap_or(Hot) { - Hot | Active | Scaled => query = query.order_by(community_aggregates::hot_rank.desc()), - NewComments | TopDay | TopTwelveHour | TopSixHour | TopHour => { - query = query.order_by(community_aggregates::users_active_day.desc()) - } - New => query = query.order_by(community::published.desc()), - Old => query = query.order_by(community::published.asc()), - // Controversial is temporary until a CommentSortType is created - MostComments | Controversial => query = query.order_by(community_aggregates::comments.desc()), - TopAll | TopYear | TopNineMonths => { - query = query.order_by(community_aggregates::subscribers.desc()) - } - TopSixMonths | TopThreeMonths => { - query = query.order_by(community_aggregates::users_active_half_year.desc()) - } - TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()), - TopWeek => query = query.order_by(community_aggregates::users_active_week.desc()), - NameAsc => query = query.order_by(lower(community::name).asc()), - NameDesc => query = query.order_by(lower(community::name).desc()), - }; - - if let Some(listing_type) = o.listing_type { - query = match listing_type { - ListingType::Subscribed => { - query.filter(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) - } - ListingType::Local => query.filter(community::local.eq(true)), - _ => query, - }; - } - - // Don't show blocked communities and communities on blocked instances. nsfw communities are - // also hidden (based on profile setting) - query = query.filter(instance_actions::blocked.is_null()); - query = query.filter(community_actions::blocked.is_null()); - if !(o.local_user.show_nsfw(site) || o.show_nsfw) { - query = query.filter(community::nsfw.eq(false)); - } - - query = o.local_user.visible_communities_only(query); - - let (limit, offset) = limit_and_offset(o.page, o.limit)?; - query - .limit(limit) - .offset(offset) - .load::(&mut conn) - .await - }; - - Queries::new(read, list) -} - impl CommunityView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(person_id: Option) -> _ { + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(community::id) + .and(community_actions::person_id.nullable().eq(person_id)), + ); + + let instance_actions_join = instance_actions::table.on( + instance_actions::instance_id + .eq(community::instance_id) + .and(instance_actions::person_id.nullable().eq(person_id)), + ); + + let local_user_join = local_user::table.on(local_user::person_id.nullable().eq(person_id)); + + community::table + .left_join(community_actions_join) + .left_join(instance_actions_join) + .left_join(local_user_join) + } + pub async fn read( pool: &mut DbPool<'_>, community_id: CommunityId, my_local_user: Option<&'_ LocalUser>, is_mod_or_admin: bool, ) -> Result { - queries() - .read(pool, (community_id, my_local_user, is_mod_or_admin)) - .await + let conn = &mut get_conn(pool).await?; + let mut query = Self::joins(my_local_user.person_id()) + .filter(community::id.eq(community_id)) + .select(Self::as_select()) + .into_boxed(); + + // Hide deleted and removed for non-admins or mods + if !is_mod_or_admin { + query = query.filter(Community::hide_removed_and_deleted()); + } + + query = my_local_user.visible_communities_only(query); + + query.first(conn).await } pub async fn check_is_mod_or_admin( @@ -217,38 +105,12 @@ impl CommunityView { } } -impl From for CommunitySortType { - fn from(value: PostSortType) -> Self { - match value { - PostSortType::Active => Self::Active, - PostSortType::Hot => Self::Hot, - PostSortType::New => Self::New, - PostSortType::Old => Self::Old, - PostSortType::TopDay => Self::TopDay, - PostSortType::TopWeek => Self::TopWeek, - PostSortType::TopMonth => Self::TopMonth, - PostSortType::TopYear => Self::TopYear, - PostSortType::TopAll => Self::TopAll, - PostSortType::MostComments => Self::MostComments, - PostSortType::NewComments => Self::NewComments, - PostSortType::TopHour => Self::TopHour, - PostSortType::TopSixHour => Self::TopSixHour, - PostSortType::TopTwelveHour => Self::TopTwelveHour, - PostSortType::TopThreeMonths => Self::TopThreeMonths, - PostSortType::TopSixMonths => Self::TopSixMonths, - PostSortType::TopNineMonths => Self::TopNineMonths, - PostSortType::Controversial => Self::Controversial, - PostSortType::Scaled => Self::Scaled, - } - } -} - #[derive(Default)] pub struct CommunityQuery<'a> { pub listing_type: Option, pub sort: Option, + pub time_range_seconds: Option, pub local_user: Option<&'a LocalUser>, - pub search_term: Option, pub title_only: Option, pub is_mod_or_admin: bool, pub show_nsfw: bool, @@ -258,7 +120,76 @@ pub struct CommunityQuery<'a> { impl CommunityQuery<'_> { pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, (self, site)).await + use CommunitySortType::*; + let conn = &mut get_conn(pool).await?; + let o = self; + + let mut query = CommunityView::joins(o.local_user.person_id()) + .select(CommunityView::as_select()) + .into_boxed(); + + // Hide deleted and removed for non-admins or mods + if !o.is_mod_or_admin { + query = query.filter(Community::hide_removed_and_deleted()).filter( + community::hidden + .eq(false) + .or(community_actions::follow_state.is_not_null()), + ); + } + + let is_subscribed = community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted)); + + if let Some(listing_type) = o.listing_type { + query = match listing_type { + ListingType::All => query.filter(community::hidden.eq(false).or(is_subscribed)), + ListingType::Subscribed => query.filter(is_subscribed), + ListingType::Local => query + .filter(community::local.eq(true)) + .filter(community::hidden.eq(false).or(is_subscribed)), + ListingType::ModeratorView => { + query.filter(community_actions::became_moderator.is_not_null()) + } + }; + } + + // Don't show blocked communities and communities on blocked instances. nsfw communities are + // also hidden (based on profile setting) + query = query.filter(instance_actions::blocked.is_null()); + query = query.filter(community_actions::blocked.is_null()); + if !(o.local_user.show_nsfw(site) || o.show_nsfw) { + query = query.filter(community::nsfw.eq(false)); + } + + query = o.local_user.visible_communities_only(query); + + match o.sort.unwrap_or_default() { + Hot => query = query.order_by(community::hot_rank.desc()), + Comments => query = query.order_by(community::comments.desc()), + Posts => query = query.order_by(community::posts.desc()), + New => query = query.order_by(community::published.desc()), + Old => query = query.order_by(community::published.asc()), + Subscribers => query = query.order_by(community::subscribers.desc()), + SubscribersLocal => query = query.order_by(community::subscribers_local.desc()), + ActiveSixMonths => query = query.order_by(community::users_active_half_year.desc()), + ActiveMonthly => query = query.order_by(community::users_active_month.desc()), + ActiveWeekly => query = query.order_by(community::users_active_week.desc()), + ActiveDaily => query = query.order_by(community::users_active_day.desc()), + NameAsc => query = query.order_by(lower(community::name).asc()), + NameDesc => query = query.order_by(lower(community::name).desc()), + }; + // Filter by the time range + if let Some(time_range_seconds) = o.time_range_seconds { + query = + query.filter(community::published.gt(now() - seconds_to_pg_interval(time_range_seconds))); + } + + let (limit, offset) = limit_and_offset(o.page, o.limit)?; + + query + .limit(limit) + .offset(offset) + .load::(conn) + .await } } @@ -266,7 +197,7 @@ impl CommunityQuery<'_> { mod tests { use crate::{ - community_view::CommunityQuery, + community::community_view::CommunityQuery, structs::{CommunitySortType, CommunityView}, }; use lemmy_db_schema::{ @@ -277,6 +208,8 @@ mod tests { CommunityFollowerForm, CommunityFollowerState, CommunityInsertForm, + CommunityModerator, + CommunityModeratorForm, CommunityUpdateForm, }, instance::Instance, @@ -284,7 +217,7 @@ mod tests { person::{Person, PersonInsertForm}, site::Site, }, - traits::{Crud, Followable}, + traits::{Crud, Followable, Joinable}, utils::{build_db_pool_for_tests, DbPool}, CommunityVisibility, SubscribedType, @@ -355,7 +288,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: url.clone().into(), + ap_id: url.clone().into(), last_refreshed_at: Default::default(), inbox_url: url.into(), private_key: None, @@ -514,4 +447,56 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn can_mod() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Make sure can_mod is false for all of them. + CommunityQuery { + local_user: Some(&data.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await? + .into_iter() + .for_each(|c| assert!(!c.can_mod)); + + let person_id = data.local_user.person_id; + + // Now join the mod team of test community 1 and 2 + let mod_form_1 = CommunityModeratorForm { + community_id: data.communities[0].id, + person_id, + }; + CommunityModerator::join(pool, &mod_form_1).await?; + + let mod_form_2 = CommunityModeratorForm { + community_id: data.communities[1].id, + person_id, + }; + CommunityModerator::join(pool, &mod_form_2).await?; + + let mod_query = CommunityQuery { + local_user: Some(&data.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await? + .into_iter() + .map(|c| (c.community.name, c.can_mod)) + .collect::>(); + + let expected_communities = vec![ + ("test_community_1".to_owned(), true), + ("test_community_2".to_owned(), true), + ("test_community_3".to_owned(), false), + ]; + assert_eq!(expected_communities, mod_query); + + cleanup(data, pool).await + } } diff --git a/crates/db_views_actor/src/lib.rs b/crates/db_views/src/community/mod.rs similarity index 56% rename from crates/db_views_actor/src/lib.rs rename to crates/db_views/src/community/mod.rs index 3f3991734..53d55c3cf 100644 --- a/crates/db_views_actor/src/lib.rs +++ b/crates/db_views/src/community/mod.rs @@ -6,10 +6,3 @@ pub mod community_moderator_view; pub mod community_person_ban_view; #[cfg(feature = "full")] pub mod community_view; -#[cfg(feature = "full")] -pub mod inbox_combined_view; -#[cfg(feature = "full")] -pub mod person_view; -#[cfg(feature = "full")] -pub mod private_message_view; -pub mod structs; diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 74a1cd5c6..342ca9463 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -2,33 +2,25 @@ extern crate serial_test; #[cfg(feature = "full")] -pub mod comment_report_view; +pub mod combined; #[cfg(feature = "full")] -pub mod comment_view; +pub mod comment; #[cfg(feature = "full")] -pub mod custom_emoji_view; +pub mod community; #[cfg(feature = "full")] -pub mod local_image_view; +pub mod local_user; #[cfg(feature = "full")] -pub mod local_user_view; +pub mod person; #[cfg(feature = "full")] -pub mod person_content_combined_view; +pub mod post; #[cfg(feature = "full")] -pub mod person_saved_combined_view; +pub mod private_message; #[cfg(feature = "full")] -pub mod post_report_view; +pub mod registration_applications; #[cfg(feature = "full")] -pub mod post_tags_view; +pub mod reports; #[cfg(feature = "full")] -pub mod post_view; -#[cfg(feature = "full")] -pub mod private_message_report_view; -#[cfg(feature = "full")] -pub mod registration_application_view; -#[cfg(feature = "full")] -pub mod report_combined_view; -#[cfg(feature = "full")] -pub mod site_view; +pub mod site; pub mod structs; #[cfg(feature = "full")] -pub mod vote_view; +pub mod utils; diff --git a/crates/db_views/src/local_user/local_user_view.rs b/crates/db_views/src/local_user/local_user_view.rs new file mode 100644 index 000000000..416fdff40 --- /dev/null +++ b/crates/db_views/src/local_user/local_user_view.rs @@ -0,0 +1,144 @@ +use crate::structs::LocalUserView; +use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest}; +use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, QueryDsl, SelectableHelper}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::{LocalUserId, OAuthProviderId, PersonId}, + schema::{local_user, oauth_account, person}, + source::{ + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + }, + traits::Crud, + utils::{ + functions::{coalesce, lower}, + get_conn, + DbPool, + }, +}; +use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; +use std::future::{ready, Ready}; + +impl LocalUserView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins() -> _ { + local_user::table.inner_join(person::table) + } + + pub async fn read(pool: &mut DbPool<'_>, local_user_id: LocalUserId) -> Result { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(local_user::id.eq(local_user_id)) + .select(Self::as_select()) + .first(conn) + .await + } + + pub async fn read_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(person::id.eq(person_id)) + .select(Self::as_select()) + .first(conn) + .await + } + + pub async fn read_from_name(pool: &mut DbPool<'_>, name: &str) -> Result { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(lower(person::name).eq(name.to_lowercase())) + .select(Self::as_select()) + .first(conn) + .await + } + + pub async fn find_by_email_or_name( + pool: &mut DbPool<'_>, + name_or_email: &str, + ) -> Result { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter( + lower(person::name) + .eq(lower(name_or_email.to_lowercase())) + .or(lower(coalesce(local_user::email, "")).eq(name_or_email.to_lowercase())), + ) + .select(Self::as_select()) + .first(conn) + .await + } + + pub async fn find_by_email(pool: &mut DbPool<'_>, from_email: &str) -> Result { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(lower(coalesce(local_user::email, "")).eq(from_email.to_lowercase())) + .select(Self::as_select()) + .first(conn) + .await + } + + pub async fn find_by_oauth_id( + pool: &mut DbPool<'_>, + oauth_provider_id: OAuthProviderId, + oauth_user_id: &str, + ) -> Result { + let conn = &mut get_conn(pool).await?; + Self::joins() + .inner_join(oauth_account::table) + .filter(oauth_account::oauth_provider_id.eq(oauth_provider_id)) + .filter(oauth_account::oauth_user_id.eq(oauth_user_id)) + .select(Self::as_select()) + .first(conn) + .await + } + + pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(local_user::email.is_not_null()) + .filter(local_user::admin.eq(true)) + .select(Self::as_select()) + .load::(conn) + .await + } + + pub async fn create_test_user( + pool: &mut DbPool<'_>, + name: &str, + bio: &str, + admin: bool, + ) -> LemmyResult { + let instance_id = Instance::read_or_create(pool, "example.com".to_string()) + .await? + .id; + let person_form = PersonInsertForm { + display_name: Some(name.to_owned()), + bio: Some(bio.to_owned()), + ..PersonInsertForm::test_form(instance_id, name) + }; + let person = Person::create(pool, &person_form).await?; + + let user_form = match admin { + true => LocalUserInsertForm::test_form_admin(person.id), + false => LocalUserInsertForm::test_form(person.id), + }; + let local_user = LocalUser::create(pool, &user_form, vec![]).await?; + + LocalUserView::read(pool, local_user.id) + .await + .with_lemmy_type(LemmyErrorType::NotFound) + } +} + +impl FromRequest for LocalUserView { + type Error = LemmyError; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + ready(match req.extensions().get::() { + Some(c) => Ok(c.clone()), + None => Err(LemmyErrorType::IncorrectLogin.into()), + }) + } +} diff --git a/crates/db_views/src/local_user/mod.rs b/crates/db_views/src/local_user/mod.rs new file mode 100644 index 000000000..ffe33f138 --- /dev/null +++ b/crates/db_views/src/local_user/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "full")] +pub mod local_user_view; diff --git a/crates/db_views/src/local_user_view.rs b/crates/db_views/src/local_user_view.rs deleted file mode 100644 index 68072cb5a..000000000 --- a/crates/db_views/src/local_user_view.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::structs::LocalUserView; -use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest}; -use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::{LocalUserId, OAuthProviderId, PersonId}, - schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates}, - source::{ - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - person::{Person, PersonInsertForm}, - }, - traits::Crud, - utils::{ - functions::{coalesce, lower}, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, -}; -use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; -use std::future::{ready, Ready}; - -enum ReadBy<'a> { - Id(LocalUserId), - Person(PersonId), - Name(&'a str), - NameOrEmail(&'a str), - Email(&'a str), - OAuthID(OAuthProviderId, &'a str), -} - -enum ListMode { - AdminsWithEmails, -} - -fn queries<'a>( -) -> Queries>, impl ListFn<'a, LocalUserView, ListMode>> { - let selection = ( - local_user::all_columns, - local_user_vote_display_mode::all_columns, - person::all_columns, - person_aggregates::all_columns, - ); - - let read = move |mut conn: DbConn<'a>, search: ReadBy<'a>| async move { - let mut query = local_user::table.into_boxed(); - query = match search { - ReadBy::Id(local_user_id) => query.filter(local_user::id.eq(local_user_id)), - ReadBy::Email(from_email) => { - query.filter(lower(coalesce(local_user::email, "")).eq(from_email.to_lowercase())) - } - _ => query, - }; - let mut query = query.inner_join(person::table); - query = match search { - ReadBy::Person(person_id) => query.filter(person::id.eq(person_id)), - ReadBy::Name(name) => query.filter(lower(person::name).eq(name.to_lowercase())), - ReadBy::NameOrEmail(name_or_email) => query.filter( - lower(person::name) - .eq(lower(name_or_email.to_lowercase())) - .or(lower(coalesce(local_user::email, "")).eq(name_or_email.to_lowercase())), - ), - _ => query, - }; - let query = query - .inner_join(local_user_vote_display_mode::table) - .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))); - - if let ReadBy::OAuthID(oauth_provider_id, oauth_user_id) = search { - query - .inner_join(oauth_account::table) - .filter(oauth_account::oauth_provider_id.eq(oauth_provider_id)) - .filter(oauth_account::oauth_user_id.eq(oauth_user_id)) - .select(selection) - .first(&mut conn) - .await - } else { - query.select(selection).first(&mut conn).await - } - }; - - let list = move |mut conn: DbConn<'a>, mode: ListMode| async move { - match mode { - ListMode::AdminsWithEmails => { - local_user::table - .inner_join(local_user_vote_display_mode::table) - .inner_join(person::table) - .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))) - .filter(local_user::email.is_not_null()) - .filter(local_user::admin.eq(true)) - .select(selection) - .load::(&mut conn) - .await - } - } - }; - - Queries::new(read, list) -} - -impl LocalUserView { - pub async fn read(pool: &mut DbPool<'_>, local_user_id: LocalUserId) -> Result { - queries().read(pool, ReadBy::Id(local_user_id)).await - } - - pub async fn read_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { - queries().read(pool, ReadBy::Person(person_id)).await - } - - pub async fn read_from_name(pool: &mut DbPool<'_>, name: &str) -> Result { - queries().read(pool, ReadBy::Name(name)).await - } - - pub async fn find_by_email_or_name( - pool: &mut DbPool<'_>, - name_or_email: &str, - ) -> Result { - queries() - .read(pool, ReadBy::NameOrEmail(name_or_email)) - .await - } - - pub async fn find_by_email(pool: &mut DbPool<'_>, from_email: &str) -> Result { - queries().read(pool, ReadBy::Email(from_email)).await - } - - pub async fn find_by_oauth_id( - pool: &mut DbPool<'_>, - oauth_provider_id: OAuthProviderId, - oauth_user_id: &str, - ) -> Result { - queries() - .read(pool, ReadBy::OAuthID(oauth_provider_id, oauth_user_id)) - .await - } - - pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, ListMode::AdminsWithEmails).await - } - - pub async fn create_test_user( - pool: &mut DbPool<'_>, - name: &str, - bio: &str, - admin: bool, - ) -> LemmyResult { - let instance_id = Instance::read_or_create(pool, "example.com".to_string()) - .await? - .id; - let person_form = PersonInsertForm { - display_name: Some(name.to_owned()), - bio: Some(bio.to_owned()), - ..PersonInsertForm::test_form(instance_id, name) - }; - let person = Person::create(pool, &person_form).await?; - - let user_form = match admin { - true => LocalUserInsertForm::test_form_admin(person.id), - false => LocalUserInsertForm::test_form(person.id), - }; - let local_user = LocalUser::create(pool, &user_form, vec![]).await?; - - LocalUserView::read(pool, local_user.id) - .await - .with_lemmy_type(LemmyErrorType::NotFound) - } -} - -impl FromRequest for LocalUserView { - type Error = LemmyError; - type Future = Ready>; - - fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { - ready(match req.extensions().get::() { - Some(c) => Ok(c.clone()), - None => Err(LemmyErrorType::IncorrectLogin.into()), - }) - } -} diff --git a/crates/db_views/src/person/mod.rs b/crates/db_views/src/person/mod.rs new file mode 100644 index 000000000..5feb31647 --- /dev/null +++ b/crates/db_views/src/person/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "full")] +pub mod person_view; diff --git a/crates/db_views/src/person/person_view.rs b/crates/db_views/src/person/person_view.rs new file mode 100644 index 000000000..995716947 --- /dev/null +++ b/crates/db_views/src/person/person_view.rs @@ -0,0 +1,213 @@ +use crate::structs::PersonView; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::PersonId, + schema::{local_user, person}, + utils::{get_conn, now, DbPool}, +}; + +impl PersonView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins() -> _ { + person::table.left_join(local_user::table) + } + + pub async fn read( + pool: &mut DbPool<'_>, + person_id: PersonId, + is_admin: bool, + ) -> Result { + let conn = &mut get_conn(pool).await?; + let mut query = Self::joins() + .filter(person::id.eq(person_id)) + .select(Self::as_select()) + .into_boxed(); + + if !is_admin { + query = query.filter(person::deleted.eq(false)) + } + + query.first(conn).await + } + + pub async fn admins(pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(person::deleted.eq(false)) + .filter(local_user::admin.eq(true)) + .order_by(person::published) + .select(Self::as_select()) + .load::(conn) + .await + } + + pub async fn banned(pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(person::deleted.eq(false)) + .filter( + person::banned.eq(true).and( + person::ban_expires + .is_null() + .or(person::ban_expires.gt(now().nullable())), + ), + ) + .order_by(person::published) + .select(Self::as_select()) + .load::(conn) + .await + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use super::*; + use lemmy_db_schema::{ + assert_length, + source::{ + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, + person::{Person, PersonInsertForm, PersonUpdateForm}, + }, + traits::Crud, + utils::build_db_pool_for_tests, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + alice: Person, + alice_local_user: LocalUser, + bob: Person, + bob_local_user: LocalUser, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let alice_form = PersonInsertForm { + local: Some(true), + ..PersonInsertForm::test_form(inserted_instance.id, "alice") + }; + let alice = Person::create(pool, &alice_form).await?; + let alice_local_user_form = LocalUserInsertForm::test_form(alice.id); + let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?; + + let bob_form = PersonInsertForm { + bot_account: Some(true), + local: Some(false), + ..PersonInsertForm::test_form(inserted_instance.id, "bob") + }; + let bob = Person::create(pool, &bob_form).await?; + let bob_local_user_form = LocalUserInsertForm::test_form(bob.id); + let bob_local_user = LocalUser::create(pool, &bob_local_user_form, vec![]).await?; + + Ok(Data { + alice, + alice_local_user, + bob, + bob_local_user, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + LocalUser::delete(pool, data.alice_local_user.id).await?; + LocalUser::delete(pool, data.bob_local_user.id).await?; + Person::delete(pool, data.alice.id).await?; + Person::delete(pool, data.bob.id).await?; + Instance::delete(pool, data.bob.instance_id).await?; + Ok(()) + } + + #[tokio::test] + #[serial] + async fn exclude_deleted() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + Person::update( + pool, + data.alice.id, + &PersonUpdateForm { + deleted: Some(true), + ..Default::default() + }, + ) + .await?; + + let read = PersonView::read(pool, data.alice.id, false).await; + assert!(read.is_err()); + + // only admin can view deleted users + let read = PersonView::read(pool, data.alice.id, true).await; + assert!(read.is_ok()); + + cleanup(data, pool).await + } + + #[tokio::test] + #[serial] + async fn list_banned() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + Person::update( + pool, + data.alice.id, + &PersonUpdateForm { + banned: Some(true), + ..Default::default() + }, + ) + .await?; + + let list = PersonView::banned(pool).await?; + assert_length!(1, list); + assert_eq!(list[0].person.id, data.alice.id); + + cleanup(data, pool).await + } + + #[tokio::test] + #[serial] + async fn list_admins() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + LocalUser::update( + pool, + data.alice_local_user.id, + &LocalUserUpdateForm { + admin: Some(true), + ..Default::default() + }, + ) + .await?; + + let list = PersonView::admins(pool).await?; + assert_length!(1, list); + assert_eq!(list[0].person.id, data.alice.id); + + let is_admin = PersonView::read(pool, data.alice.id, false).await?.is_admin; + assert!(is_admin); + + let is_admin = PersonView::read(pool, data.bob.id, false).await?.is_admin; + assert!(!is_admin); + + cleanup(data, pool).await + } +} diff --git a/crates/db_views/src/post/mod.rs b/crates/db_views/src/post/mod.rs new file mode 100644 index 000000000..fec40b095 --- /dev/null +++ b/crates/db_views/src/post/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "full")] +pub mod post_tags_view; +#[cfg(feature = "full")] +pub mod post_view; diff --git a/crates/db_views/src/post_tags_view.rs b/crates/db_views/src/post/post_tags_view.rs similarity index 100% rename from crates/db_views/src/post_tags_view.rs rename to crates/db_views/src/post/post_tags_view.rs diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post/post_view.rs similarity index 69% rename from crates/db_views/src/post_view.rs rename to crates/db_views/src/post/post_view.rs index f04747cfd..11ee0790c 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post/post_view.rs @@ -1,7 +1,10 @@ -use crate::structs::{PaginationCursor, PostView}; +use crate::{ + structs::{PostPaginationCursor, PostView}, + utils::filter_blocked, +}; use diesel::{ debug_query, - dsl::{exists, not, IntervalDsl}, + dsl::{exists, not}, pg::Pg, query_builder::AsQuery, result::Error, @@ -16,10 +19,12 @@ use diesel::{ }; use diesel_async::RunQueryDsl; 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}, + aliases::{creator_community_actions, creator_local_user}, + impls::{ + community::community_follower_select_subscribed_type, + local_user::{local_user_can_mod, LocalUserOptionHelper}, + }, + newtypes::{CommunityId, PersonId, PostId}, schema::{ community, community_actions, @@ -31,32 +36,26 @@ use lemmy_db_schema::{ person_actions, post, post_actions, - post_aggregates, post_tag, tag, }, source::{ - community::{CommunityFollower, CommunityFollowerState}, + community::CommunityFollowerState, local_user::LocalUser, - post::{post_actions_keys, PostActionsCursor}, + post::{post_actions_keys, post_keys as key, Post, PostActionsCursor}, site::Site, }, + traits::Crud, utils::{ - action_query, - actions, - actions_alias, functions::coalesce, fuzzy_search, get_conn, limit_and_offset, now, paginate, + seconds_to_pg_interval, Commented, - DbConn, DbPool, - ListFn, - Queries, - ReadFn, ReverseTimestampKey, }, CommunityVisibility, @@ -66,76 +65,96 @@ use lemmy_db_schema::{ use tracing::debug; use PostSortType::*; -type QueriesReadTypes<'a> = (PostId, Option<&'a LocalUser>, bool); -type QueriesListTypes<'a> = (PostQuery<'a>, &'a Site); +impl PostView { + // TODO while we can abstract the joins into a function, the selects are currently impossible to + // do, because they rely on a few types that aren't yet publicly exported in diesel: + // https://github.com/diesel-rs/diesel/issues/4462 -fn queries<'a>() -> Queries< - impl ReadFn<'a, PostView, QueriesReadTypes<'a>>, - impl ListFn<'a, PostView, QueriesListTypes<'a>>, -> { - let creator_is_admin = exists( - local_user::table.filter( - post_aggregates::creator_id - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ); + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(my_person_id: Option) -> _ { + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(post::community_id) + .and(community_actions::person_id.nullable().eq(my_person_id)), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(post::creator_id) + .and(person_actions::person_id.nullable().eq(my_person_id)), + ); + + let post_actions_join = post_actions::table.on( + post_actions::post_id + .eq(post::id) + .and(post_actions::person_id.nullable().eq(my_person_id)), + ); + + let instance_actions_join = instance_actions::table.on( + instance_actions::instance_id + .eq(post::instance_id) + .and(instance_actions::person_id.nullable().eq(my_person_id)), + ); + + let post_creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(post::creator_id), + ), + ); + + let image_details_join = + image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())); + + let local_user_join = local_user::table.on(local_user::person_id.nullable().eq(my_person_id)); + + post::table + .inner_join(person::table) + .inner_join(community::table) + .left_join(image_details_join) + .left_join(community_actions_join) + .left_join(person_actions_join) + .left_join(post_actions_join) + .left_join(instance_actions_join) + .left_join(post_creator_community_actions_join) + .left_join(local_user_join) + } + + #[diesel::dsl::auto_type(no_type_alias)] + fn creator_is_admin() -> _ { + exists( + creator_local_user.filter( + post::creator_id + .eq(creator_local_user.field(local_user::person_id)) + .and(creator_local_user.field(local_user::admin).eq(true)), + ), + ) + } + + pub async fn read( + pool: &mut DbPool<'_>, + post_id: PostId, + my_local_user: Option<&'_ LocalUser>, + is_mod_or_admin: bool, + ) -> Result { + let conn = &mut get_conn(pool).await?; + let my_person_id = my_local_user.person_id(); - // TODO maybe this should go to localuser also - let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, - my_person_id: Option| { - // 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 - // data transferred (since each post will be returned once per tag that it has) and more - // complicated application code. The diesel docs suggest doing three separate sequential queries - // in this case (see https://diesel.rs/guides/relations.html#many-to-many-or-mn ): First fetch - // the posts, then fetch all relevant post-tag-association tuples from the db, and then fetch - // all the relevant tag objects. - // - // If we want to filter by post tag we will have to add - // separate logic below since this subquery can't affect filtering, but it is simple (`WHERE - // exists (select 1 from post_community_post_tags where community_post_tag_id in (1,2,3,4)`). let post_tags = post_tag::table .inner_join(tag::table) .select(diesel::dsl::sql::( "json_agg(tag.*)", )) - .filter(post_tag::post_id.eq(post_aggregates::post_id)) + .filter(post_tag::post_id.eq(post::id)) .filter(tag::deleted.eq(false)) .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(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, - )) + let mut query = Self::joins(my_person_id) + .filter(post::id.eq(post_id)) .select(( post::all_columns, person::all_columns, @@ -150,38 +169,21 @@ fn queries<'a>() -> Queries< .field(community_actions::became_moderator) .nullable() .is_not_null(), - creator_is_admin, - post_aggregates::all_columns, - CommunityFollower::select_subscribed_type(), - post_actions::saved.nullable().is_not_null(), + Self::creator_is_admin(), + community_follower_select_subscribed_type(), + post_actions::saved.nullable(), 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() - post_actions::read_comments_amount.nullable(), - post_aggregates::comments, + post::comments.nullable() - post_actions::read_comments_amount.nullable(), + post::comments, ), post_tags, + local_user_can_mod(), )) - }; - - let read = move |mut conn: DbConn<'a>, - (post_id, my_local_user, is_mod_or_admin): ( - PostId, - Option<&'a LocalUser>, - bool, - )| async move { - // The left join below will return None in this case - let my_person_id = my_local_user.person_id(); - let person_id_join = my_person_id.unwrap_or(PersonId(-1)); - - let mut query = all_joins( - post_aggregates::table - .filter(post_aggregates::post_id.eq(post_id)) - .into_boxed(), - my_person_id, - ); + .into_boxed(); // Hide deleted and removed for non-admins or mods if !is_mod_or_admin { @@ -189,23 +191,23 @@ fn queries<'a>() -> Queries< .filter( community::removed .eq(false) - .or(post::creator_id.eq(person_id_join)), + .or(post::creator_id.nullable().eq(my_person_id)), ) .filter( post::removed .eq(false) - .or(post::creator_id.eq(person_id_join)), + .or(post::creator_id.nullable().eq(my_person_id)), ) // users can see their own deleted posts .filter( community::deleted .eq(false) - .or(post::creator_id.eq(person_id_join)), + .or(post::creator_id.nullable().eq(my_person_id)), ) .filter( post::deleted .eq(false) - .or(post::creator_id.eq(person_id_join)), + .or(post::creator_id.nullable().eq(my_person_id)), ) // private communities can only by browsed by accepted followers .filter( @@ -219,18 +221,213 @@ fn queries<'a>() -> Queries< Commented::new(query) .text("PostView::read") - .first(&mut conn) + .first(conn) .await - }; + } +} - let list = move |mut conn: DbConn<'a>, (o, site): (PostQuery<'a>, &'a Site)| async move { - // The left join below will return None in this case - let local_user_id_join = o.local_user.local_user_id().unwrap_or(LocalUserId(-1)); - - let mut query = all_joins( - post_aggregates::table.into_boxed(), - o.local_user.person_id(), +// TODO This pagination cursor is a mess, get rid of it and have it match the others +impl PostPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &PostView) -> PostPaginationCursor { + // hex encoding to prevent ossification + PostPaginationCursor(format!("P{:x}", view.post.id.0)) + } + pub async fn read( + &self, + pool: &mut DbPool<'_>, + local_user: Option<&LocalUser>, + ) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let post_id = PostId( + self + .0 + .get(1..) + .and_then(|e| i32::from_str_radix(e, 16).ok()) + .ok_or_else(err_msg)?, ); + let post = Post::read(pool, post_id).await?; + let post_actions = PostActionsCursor::read(pool, post_id, local_user.person_id()).await?; + + Ok(PaginationCursorData { post, post_actions }) + } +} + +// currently we use aggregates or actions as the pagination token. +// we only use some of the properties, depending on which sort type we page by +#[derive(Clone)] +pub struct PaginationCursorData { + post: Post, + post_actions: PostActionsCursor, +} + +#[derive(Clone, Default)] +pub struct PostQuery<'a> { + pub listing_type: Option, + pub sort: Option, + pub time_range_seconds: Option, + pub creator_id: Option, + pub community_id: Option, + // if true, the query should be handled as if community_id was not given except adding the + // literal filter + pub community_id_just_for_prefetch: bool, + pub local_user: Option<&'a LocalUser>, + // TODO get rid of this + pub search_term: Option, + pub url_only: Option, + pub read_only: Option, + pub liked_only: Option, + pub disliked_only: Option, + pub title_only: Option, + pub page: Option, + pub limit: Option, + // TODO these should be simple cursors like the others, not data + pub page_after: Option, + pub page_before_or_equal: Option, + pub page_back: Option, + pub show_hidden: Option, + pub show_read: Option, + pub show_nsfw: Option, + pub hide_media: Option, + pub no_comments_only: Option, +} + +impl<'a> PostQuery<'a> { + // TODO this should not be doing recursive fetching, get rid of it. + #[allow(clippy::expect_used)] + async fn prefetch_upper_bound_for_page_before( + &self, + site: &Site, + pool: &mut DbPool<'_>, + ) -> Result>, Error> { + // first get one page for the most popular community to get an upper bound for the page end for + // the real query. the reason this is needed is that when fetching posts for a single + // community PostgreSQL can optimize the query to use an index on e.g. (=, >=, >=, >=) and + // fetch only LIMIT rows but for the followed-communities query it has to query the index on + // (IN, >=, >=, >=) which it currently can't do at all (as of PG 16). see the discussion + // here: https://github.com/LemmyNet/lemmy/issues/2877#issuecomment-1673597190 + // + // the results are correct no matter which community we fetch these for, since it basically + // covers the "worst case" of the whole page consisting of posts from one community + // but using the largest community decreases the pagination-frame so make the real query more + // efficient. + let (limit, offset) = limit_and_offset(self.page, self.limit)?; + if offset != 0 && self.page_after.is_some() { + return Err(Error::QueryBuilderError( + "legacy pagination cannot be combined with v2 pagination".into(), + )); + } + 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_actions::table + .filter(community_actions::followed.is_not_null()) + .filter(community_actions::person_id.eq(self_person_id)) + .inner_join(community::table.on(community::id.eq(community_actions::community_id))) + .order_by(community::users_active_month.desc()) + .select(community::id) + .limit(1) + .get_result::(conn) + .await + .optional()? + }; + let Some(largest_subscribed) = largest_subscribed else { + // nothing subscribed to? no posts + return Ok(None); + }; + + let mut v = Box::pin( + PostQuery { + community_id: Some(largest_subscribed), + community_id_just_for_prefetch: true, + ..self.clone() + } + .list(site, pool), + ) + .await?; + + // take last element of array. if this query returned less than LIMIT elements, + // the heuristic is invalid since we can't guarantee the full query will return >= LIMIT results + // (return original query) + if (v.len() as i64) < limit { + Ok(Some(self.clone())) + } else { + let item = if self.page_back.unwrap_or_default() { + // for backward pagination, get first element instead + v.into_iter().next() + } else { + v.pop() + }; + let limit_cursor = Some(item.expect("else case").post); + Ok(Some(PostQuery { + page_before_or_equal: limit_cursor, + ..self.clone() + })) + } + } + + pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { + let o = if self.listing_type == Some(ListingType::Subscribed) + && self.community_id.is_none() + && self.local_user.is_some() + && self.page_before_or_equal.is_none() + { + if let Some(query) = self + .prefetch_upper_bound_for_page_before(site, pool) + .await? + { + query + } else { + self + } + } else { + self + }; + + let conn = &mut get_conn(pool).await?; + + let my_person_id = o.local_user.person_id(); + let my_local_user_id = o.local_user.local_user_id(); + + let post_tags = post_tag::table + .inner_join(tag::table) + .select(diesel::dsl::sql::( + "json_agg(tag.*)", + )) + .filter(post_tag::post_id.eq(post::id)) + .filter(tag::deleted.eq(false)) + .single_value(); + + let mut query = PostView::joins(my_person_id) + .select(( + post::all_columns, + person::all_columns, + community::all_columns, + image_details::all_columns.nullable(), + 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(), + PostView::creator_is_admin(), + community_follower_select_subscribed_type(), + post_actions::saved.nullable(), + 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::comments.nullable() - post_actions::read_comments_amount.nullable(), + post::comments, + ), + post_tags, + local_user_can_mod(), + )) + .into_boxed(); // hide posts from deleted communities query = query.filter(community::deleted.eq(false)); @@ -256,11 +453,11 @@ fn queries<'a>() -> Queries< .filter(post::removed.eq(false)); } if let Some(community_id) = o.community_id { - query = query.filter(post_aggregates::community_id.eq(community_id)); + query = query.filter(post::community_id.eq(community_id)); } if let Some(creator_id) = o.creator_id { - query = query.filter(post_aggregates::creator_id.eq(creator_id)); + query = query.filter(post::creator_id.eq(creator_id)); } let is_subscribed = community_actions::followed.is_not_null(); @@ -278,16 +475,23 @@ fn queries<'a>() -> Queries< } if let Some(search_term) = &o.search_term { + let url_filter = post::url.eq(search_term); if o.url_only.unwrap_or_default() { - query = query.filter(post::url.eq(search_term)); + query = query.filter(url_filter); } else { let searcher = fuzzy_search(search_term); let name_filter = post::name.ilike(searcher.clone()); let body_filter = post::body.ilike(searcher.clone()); + let alt_text_filter = post::alt_text.ilike(searcher.clone()); query = if o.title_only.unwrap_or_default() { query.filter(name_filter) } else { - query.filter(name_filter.or(body_filter)) + query.filter( + name_filter + .or(body_filter) + .or(alt_text_filter) + .or(url_filter), + ) } .filter(not(post::removed.or(post::deleted))); } @@ -305,7 +509,7 @@ fn queries<'a>() -> Queries< // Filter to show only posts with no comments if o.no_comments_only.unwrap_or_default() { - query = query.filter(post_aggregates::comments.eq(0)); + query = query.filter(post::comments.eq(0)); }; if !o.show_read.unwrap_or(o.local_user.show_read_posts()) { @@ -332,7 +536,7 @@ fn queries<'a>() -> Queries< } if let Some(my_id) = o.local_user.person_id() { - let not_creator_filter = post_aggregates::creator_id.ne(my_id); + let not_creator_filter = post::creator_id.ne(my_id); if o.liked_only.unwrap_or_default() { query = query .filter(not_creator_filter) @@ -360,17 +564,16 @@ fn queries<'a>() -> Queries< if o.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)), + post::language_id.eq(local_user_language::language_id).and( + local_user_language::local_user_id + .nullable() + .eq(my_local_user_id), + ), ), )); } - // Don't show blocked instances, communities or persons - query = query.filter(community_actions::blocked.is_null()); - query = query.filter(instance_actions::blocked.is_null()); - query = query.filter(person_actions::blocked.is_null()); + query = query.filter(filter_blocked()); } let (limit, offset) = limit_and_offset(o.page, o.limit)?; @@ -389,7 +592,7 @@ fn queries<'a>() -> Queries< } else { let mut query = paginate( query, - o.page_after.map(|c| c.post_aggregates), + o.page_after.map(|c| c.post), o.page_before_or_equal, o.page_back.unwrap_or_default(), ); @@ -401,8 +604,6 @@ fn queries<'a>() -> Queries< query.then_desc(key::featured_community) }; - let time = |interval| post_aggregates::published.gt(now() - interval); - // then use the main sort query = match o.sort.unwrap_or(Hot) { Active => query.then_desc(key::hot_rank_active), @@ -413,19 +614,15 @@ fn queries<'a>() -> Queries< Old => query.then_desc(ReverseTimestampKey(key::published)), NewComments => query.then_desc(key::newest_comment_time), MostComments => query.then_desc(key::comments), - TopAll => query.then_desc(key::score), - TopYear => query.then_desc(key::score).filter(time(1.years())), - TopMonth => query.then_desc(key::score).filter(time(1.months())), - TopWeek => query.then_desc(key::score).filter(time(1.weeks())), - TopDay => query.then_desc(key::score).filter(time(1.days())), - TopHour => query.then_desc(key::score).filter(time(1.hours())), - TopSixHour => query.then_desc(key::score).filter(time(6.hours())), - TopTwelveHour => query.then_desc(key::score).filter(time(12.hours())), - TopThreeMonths => query.then_desc(key::score).filter(time(3.months())), - TopSixMonths => query.then_desc(key::score).filter(time(6.months())), - TopNineMonths => query.then_desc(key::score).filter(time(9.months())), + Top => query.then_desc(key::score), }; + // Filter by the time range + if let Some(time_range_seconds) = o.time_range_seconds { + query = + query.filter(post::published.gt(now() - seconds_to_pg_interval(time_range_seconds))); + } + // use publish as fallback. especially useful for hot rank which reaches zero after some days. // necessary because old posts can be fetched over federation and inserted with high post id query = match o.sort.unwrap_or(Hot) { @@ -435,7 +632,7 @@ fn queries<'a>() -> Queries< }; // finally use unique post id as tie breaker - query = query.then_desc(key::post_id); + query = query.then_desc(key::id); query.as_query() }; @@ -448,188 +645,8 @@ fn queries<'a>() -> Queries< "getting upper bound for next query", o.community_id_just_for_prefetch, ) - .load::(&mut conn) + .load::(conn) .await - }; - - Queries::new(read, list) -} - -impl PostView { - pub async fn read( - pool: &mut DbPool<'_>, - post_id: PostId, - my_local_user: Option<&'_ LocalUser>, - is_mod_or_admin: bool, - ) -> Result { - queries() - .read(pool, (post_id, my_local_user, is_mod_or_admin)) - .await - } -} - -impl PaginationCursor { - // get cursor for page that starts immediately after the given post - pub fn after_post(view: &PostView) -> PaginationCursor { - // hex encoding to prevent ossification - PaginationCursor(format!("P{:x}", view.counts.post_id.0)) - } - pub async fn read( - &self, - pool: &mut DbPool<'_>, - local_user: Option<&LocalUser>, - ) -> Result { - let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); - let post_id = PostId( - self - .0 - .get(1..) - .and_then(|e| i32::from_str_radix(e, 16).ok()) - .ok_or_else(err_msg)?, - ); - let post_aggregates = PostAggregates::read(pool, post_id).await?; - let post_actions = PostActionsCursor::read(pool, post_id, local_user.person_id()).await?; - - Ok(PaginationCursorData { - post_aggregates, - post_actions, - }) - } -} - -// currently we use aggregates or actions as the pagination token. -// we only use some of the properties, depending on which sort type we page by -#[derive(Clone)] -pub struct PaginationCursorData { - post_aggregates: PostAggregates, - post_actions: PostActionsCursor, -} - -#[derive(Clone, Default)] -pub struct PostQuery<'a> { - pub listing_type: Option, - pub sort: Option, - pub creator_id: Option, - pub community_id: Option, - // if true, the query should be handled as if community_id was not given except adding the - // literal filter - pub community_id_just_for_prefetch: bool, - pub local_user: Option<&'a LocalUser>, - pub search_term: Option, - pub url_only: Option, - pub read_only: Option, - pub liked_only: Option, - pub disliked_only: Option, - pub title_only: Option, - pub page: Option, - pub limit: Option, - pub page_after: Option, - pub page_before_or_equal: Option, - pub page_back: Option, - pub show_hidden: Option, - pub show_read: Option, - pub show_nsfw: Option, - pub hide_media: Option, - pub no_comments_only: Option, -} - -impl<'a> PostQuery<'a> { - #[allow(clippy::expect_used)] - async fn prefetch_upper_bound_for_page_before( - &self, - site: &Site, - pool: &mut DbPool<'_>, - ) -> Result>, Error> { - // first get one page for the most popular community to get an upper bound for the page end for - // the real query. the reason this is needed is that when fetching posts for a single - // community PostgreSQL can optimize the query to use an index on e.g. (=, >=, >=, >=) and - // fetch only LIMIT rows but for the followed-communities query it has to query the index on - // (IN, >=, >=, >=) which it currently can't do at all (as of PG 16). see the discussion - // here: https://github.com/LemmyNet/lemmy/issues/2877#issuecomment-1673597190 - // - // the results are correct no matter which community we fetch these for, since it basically - // covers the "worst case" of the whole page consisting of posts from one community - // but using the largest community decreases the pagination-frame so make the real query more - // efficient. - 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() { - return Err(Error::QueryBuilderError( - "legacy pagination cannot be combined with v2 pagination".into(), - )); - } - 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?; - 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) - .get_result::(conn) - .await - .optional()? - }; - let Some(largest_subscribed) = largest_subscribed else { - // nothing subscribed to? no posts - return Ok(None); - }; - - let mut v = queries() - .list( - pool, - ( - PostQuery { - community_id: Some(largest_subscribed), - community_id_just_for_prefetch: true, - ..self.clone() - }, - site, - ), - ) - .await?; - // take last element of array. if this query returned less than LIMIT elements, - // the heuristic is invalid since we can't guarantee the full query will return >= LIMIT results - // (return original query) - if (v.len() as i64) < limit { - Ok(Some(self.clone())) - } else { - let item = if self.page_back.unwrap_or_default() { - // for backward pagination, get first element instead - v.into_iter().next() - } else { - v.pop() - }; - let limit_cursor = Some(item.expect("else case").counts); - Ok(Some(PostQuery { - page_before_or_equal: limit_cursor, - ..self.clone() - })) - } - } - - pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { - if self.listing_type == Some(ListingType::Subscribed) - && self.community_id.is_none() - && self.local_user.is_some() - && self.page_before_or_equal.is_none() - { - if let Some(query) = self - .prefetch_upper_bound_for_page_before(site, pool) - .await? - { - queries().list(pool, (query, site)).await - } else { - Ok(vec![]) - } - } else { - queries().list(pool, (self, site)).await - } } } @@ -638,13 +655,12 @@ impl<'a> PostQuery<'a> { #[cfg(test)] mod tests { use crate::{ - post_view::{PaginationCursorData, PostQuery, PostView}, + post::post_view::{PaginationCursorData, PostQuery, PostView}, structs::{LocalUserView, PostTags}, }; use chrono::Utc; use diesel_async::SimpleAsyncConnection; use lemmy_db_schema::{ - aggregates::structs::PostAggregates, impls::actor_language::UNDETERMINED_ID, newtypes::LanguageId, source::{ @@ -667,7 +683,6 @@ mod tests { instance_block::{InstanceBlock, InstanceBlockForm}, language::Language, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, person::{Person, PersonInsertForm}, person_block::{PersonBlock, PersonBlockForm}, post::{ @@ -708,14 +723,14 @@ mod tests { struct Data { pool: ActualDbPool, - inserted_instance: Instance, - local_user_view: LocalUserView, - blocked_local_user_view: LocalUserView, - inserted_bot: Person, - inserted_community: Community, - inserted_post: Post, - inserted_bot_post: Post, - inserted_post_with_tags: Post, + instance: Instance, + tegan_local_user_view: LocalUserView, + john_local_user_view: LocalUserView, + bot_local_user_view: LocalUserView, + community: Community, + post: Post, + bot_post: Post, + post_with_tags: Post, tag_1: Tag, tag_2: Tag, site: Site, @@ -731,7 +746,7 @@ mod tests { fn default_post_query(&self) -> PostQuery<'_> { PostQuery { sort: Some(PostSortType::New), - local_user: Some(&self.local_user_view.local_user), + local_user: Some(&self.tegan_local_user_view.local_user), ..Default::default() } } @@ -739,41 +754,43 @@ mod tests { async fn setup() -> LemmyResult { let actual_pool = build_db_pool()?; let pool = &mut (&actual_pool).into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let new_person = PersonInsertForm::test_form(inserted_instance.id, "tegan"); - - let inserted_person = Person::create(pool, &new_person).await?; - - let local_user_form = LocalUserInsertForm { + let tegan_person_form = PersonInsertForm::test_form(instance.id, "tegan"); + let inserted_tegan_person = Person::create(pool, &tegan_person_form).await?; + let tegan_local_user_form = LocalUserInsertForm { admin: Some(true), - ..LocalUserInsertForm::test_form(inserted_person.id) + ..LocalUserInsertForm::test_form(inserted_tegan_person.id) }; - let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; + let inserted_tegan_local_user = + LocalUser::create(pool, &tegan_local_user_form, vec![]).await?; - let new_bot = PersonInsertForm { + let bot_person_form = PersonInsertForm { bot_account: Some(true), - ..PersonInsertForm::test_form(inserted_instance.id, "mybot") + ..PersonInsertForm::test_form(instance.id, "mybot") }; - - let inserted_bot = Person::create(pool, &new_bot).await?; + let inserted_bot_person = Person::create(pool, &bot_person_form).await?; + let inserted_bot_local_user = LocalUser::create( + pool, + &LocalUserInsertForm::test_form(inserted_bot_person.id), + vec![], + ) + .await?; let new_community = CommunityInsertForm::new( - inserted_instance.id, + instance.id, "test_community_3".to_string(), "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await?; + let community = Community::create(pool, &new_community).await?; // Test a person block, make sure the post query doesn't include their post - let blocked_person = PersonInsertForm::test_form(inserted_instance.id, "john"); - - let inserted_blocked_person = Person::create(pool, &blocked_person).await?; - - let inserted_blocked_local_user = LocalUser::create( + let john_person_form = PersonInsertForm::test_form(instance.id, "john"); + let inserted_john_person = Person::create(pool, &john_person_form).await?; + let inserted_john_local_user = LocalUser::create( pool, - &LocalUserInsertForm::test_form(inserted_blocked_person.id), + &LocalUserInsertForm::test_form(inserted_john_person.id), vec![], ) .await?; @@ -782,16 +799,16 @@ mod tests { language_id: Some(LanguageId(1)), ..PostInsertForm::new( POST_BY_BLOCKED_PERSON.to_string(), - inserted_blocked_person.id, - inserted_community.id, + inserted_john_person.id, + community.id, ) }; Post::create(pool, &post_from_blocked_person).await?; // block that person let person_block = PersonBlockForm { - person_id: inserted_person.id, - target_id: inserted_blocked_person.id, + person_id: inserted_tegan_person.id, + target_id: inserted_john_person.id, }; PersonBlock::block(pool, &person_block).await?; @@ -800,9 +817,9 @@ mod tests { let tag_1 = Tag::create( pool, &TagInsertForm { - ap_id: Url::parse(&format!("{}/tags/test_tag1", inserted_community.actor_id))?.into(), + ap_id: Url::parse(&format!("{}/tags/test_tag1", community.ap_id))?.into(), name: "Test Tag 1".into(), - community_id: inserted_community.id, + community_id: community.id, published: None, updated: None, deleted: false, @@ -812,9 +829,9 @@ mod tests { let tag_2 = Tag::create( pool, &TagInsertForm { - ap_id: Url::parse(&format!("{}/tags/test_tag2", inserted_community.actor_id))?.into(), + ap_id: Url::parse(&format!("{}/tags/test_tag2", community.ap_id))?.into(), name: "Test Tag 2".into(), - community_id: inserted_community.id, + community_id: community.id, published: None, updated: None, deleted: false, @@ -825,52 +842,53 @@ mod tests { // A sample post let new_post = PostInsertForm { language_id: Some(LanguageId(47)), - ..PostInsertForm::new(POST.to_string(), inserted_person.id, inserted_community.id) + ..PostInsertForm::new(POST.to_string(), inserted_tegan_person.id, community.id) }; - let inserted_post = Post::create(pool, &new_post).await?; + let post = Post::create(pool, &new_post).await?; let new_bot_post = PostInsertForm::new( POST_BY_BOT.to_string(), - inserted_bot.id, - inserted_community.id, + inserted_bot_person.id, + community.id, ); - let inserted_bot_post = Post::create(pool, &new_bot_post).await?; + let bot_post = Post::create(pool, &new_bot_post).await?; // A sample post with tags let new_post = PostInsertForm { language_id: Some(LanguageId(47)), ..PostInsertForm::new( POST_WITH_TAGS.to_string(), - inserted_person.id, - inserted_community.id, + inserted_tegan_person.id, + community.id, ) }; - let inserted_post_with_tags = Post::create(pool, &new_post).await?; + let post_with_tags = Post::create(pool, &new_post).await?; let inserted_tags = vec![ PostTagInsertForm { - post_id: inserted_post_with_tags.id, + post_id: post_with_tags.id, tag_id: tag_1.id, }, PostTagInsertForm { - post_id: inserted_post_with_tags.id, + post_id: post_with_tags.id, tag_id: tag_2.id, }, ]; PostTagInsertForm::insert_tag_associations(pool, &inserted_tags).await?; - let local_user_view = LocalUserView { - local_user: inserted_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_person, - counts: Default::default(), + let tegan_local_user_view = LocalUserView { + local_user: inserted_tegan_local_user, + person: inserted_tegan_person, }; - let blocked_local_user_view = LocalUserView { - local_user: inserted_blocked_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_blocked_person, - counts: Default::default(), + let john_local_user_view = LocalUserView { + local_user: inserted_john_local_user, + person: inserted_john_person, + }; + + let bot_local_user_view = LocalUserView { + local_user: inserted_bot_local_user, + person: inserted_bot_person, }; let site = Site { @@ -882,7 +900,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("http://example.com")?.into(), + ap_id: Url::parse("http://example.com")?.into(), last_refreshed_at: Default::default(), inbox_url: Url::parse("http://example.com")?.into(), private_key: None, @@ -893,14 +911,14 @@ mod tests { Ok(Data { pool: actual_pool, - inserted_instance, - local_user_view, - blocked_local_user_view, - inserted_bot, - inserted_community, - inserted_post, - inserted_bot_post, - inserted_post_with_tags, + instance, + tegan_local_user_view, + john_local_user_view, + bot_local_user_view, + community, + post, + bot_post, + post_with_tags, tag_1, tag_2, site, @@ -909,12 +927,12 @@ mod tests { async fn teardown(data: Data) -> LemmyResult<()> { let pool = &mut data.pool2(); // let pool = &mut (&pool).into(); - let num_deleted = Post::delete(pool, data.inserted_post.id).await?; - Community::delete(pool, data.inserted_community.id).await?; - Person::delete(pool, data.local_user_view.person.id).await?; - Person::delete(pool, data.inserted_bot.id).await?; - Person::delete(pool, data.blocked_local_user_view.person.id).await?; - Instance::delete(pool, data.inserted_instance.id).await?; + let num_deleted = Post::delete(pool, data.post.id).await?; + Community::delete(pool, data.community.id).await?; + Person::delete(pool, data.tegan_local_user_view.person.id).await?; + Person::delete(pool, data.bot_local_user_view.person.id).await?; + Person::delete(pool, data.john_local_user_view.person.id).await?; + Instance::delete(pool, data.instance.id).await?; assert_eq!(1, num_deleted); Ok(()) @@ -940,11 +958,16 @@ mod tests { show_bot_accounts: Some(false), ..Default::default() }; - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user.show_bot_accounts = false; + LocalUser::update( + pool, + data.tegan_local_user_view.local_user.id, + &local_user_form, + ) + .await?; + data.tegan_local_user_view.local_user.show_bot_accounts = false; let mut read_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), ..data.default_post_query() } .list(&data.site, pool) @@ -954,13 +977,13 @@ mod tests { let post_listing_single_with_person = PostView::read( pool, - data.inserted_post.id, - Some(&data.local_user_view.local_user), + data.post.id, + Some(&data.tegan_local_user_view.local_user), false, ) .await?; - let expected_post_listing_with_user = expected_post_view(data, pool).await?; + let expected_post_listing_with_user = expected_post_view(data)?; // Should be only one person, IE the bot post, and blocked should be missing assert_eq!( @@ -976,11 +999,16 @@ mod tests { show_bot_accounts: Some(true), ..Default::default() }; - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user.show_bot_accounts = true; + LocalUser::update( + pool, + data.tegan_local_user_view.local_user.id, + &local_user_form, + ) + .await?; + data.tegan_local_user_view.local_user.show_bot_accounts = true; let post_listings_with_bots = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), ..data.default_post_query() } .list(&data.site, pool) @@ -1001,7 +1029,7 @@ mod tests { let pool = &mut pool.into(); let read_post_listing_multiple_no_person = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), local_user: None, ..data.default_post_query() } @@ -1009,9 +1037,10 @@ mod tests { .await?; let read_post_listing_single_no_person = - PostView::read(pool, data.inserted_post.id, None, false).await?; + PostView::read(pool, data.post.id, None, false).await?; - let expected_post_listing_no_person = expected_post_view(data, pool).await?; + let mut expected_post_listing_no_person = expected_post_view(data)?; + expected_post_listing_no_person.can_mod = false; // Should be 2 posts, with the bot post, and the blocked assert_eq!( @@ -1043,15 +1072,15 @@ mod tests { body: Some("Post".to_string()), ..PostInsertForm::new( POST_WITH_ANOTHER_TITLE.to_string(), - data.local_user_view.person.id, - data.inserted_community.id, + data.tegan_local_user_view.person.id, + data.community.id, ) }; let inserted_post = Post::create(pool, &new_post).await?; let read_post_listing_by_title_only = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), local_user: None, search_term: Some("Post".to_string()), title_only: Some(true), @@ -1061,7 +1090,7 @@ mod tests { .await?; let read_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), local_user: None, search_term: Some("Post".to_string()), ..data.default_post_query() @@ -1098,13 +1127,13 @@ mod tests { let pool = &mut pool.into(); let community_block = CommunityBlockForm { - person_id: data.local_user_view.person.id, - community_id: data.inserted_community.id, + person_id: data.tegan_local_user_view.person.id, + community_id: data.community.id, }; CommunityBlock::block(pool, &community_block).await?; let read_post_listings_with_person_after_block = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), ..data.default_post_query() } .list(&data.site, pool) @@ -1123,14 +1152,13 @@ mod tests { let pool = &data.pool(); let pool = &mut pool.into(); - let post_like_form = - PostLikeForm::new(data.inserted_post.id, data.local_user_view.person.id, 1); + let post_like_form = PostLikeForm::new(data.post.id, data.tegan_local_user_view.person.id, 1); let inserted_post_like = PostLike::like(pool, &post_like_form).await?; let expected_post_like = PostLike { - post_id: data.inserted_post.id, - person_id: data.local_user_view.person.id, + post_id: data.post.id, + person_id: data.tegan_local_user_view.person.id, published: inserted_post_like.published, score: 1, }; @@ -1138,27 +1166,33 @@ mod tests { let post_listing_single_with_person = PostView::read( pool, - data.inserted_post.id, - Some(&data.local_user_view.local_user), + data.post.id, + Some(&data.tegan_local_user_view.local_user), false, ) .await?; - let mut expected_post_with_upvote = expected_post_view(data, pool).await?; + let mut expected_post_with_upvote = expected_post_view(data)?; expected_post_with_upvote.my_vote = Some(1); - expected_post_with_upvote.counts.score = 1; - expected_post_with_upvote.counts.upvotes = 1; + expected_post_with_upvote.post.score = 1; + expected_post_with_upvote.post.upvotes = 1; + expected_post_with_upvote.creator.post_score = 1; assert_eq!(expected_post_with_upvote, post_listing_single_with_person); let local_user_form = LocalUserUpdateForm { show_bot_accounts: Some(false), ..Default::default() }; - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user.show_bot_accounts = false; + LocalUser::update( + pool, + data.tegan_local_user_view.local_user.id, + &local_user_form, + ) + .await?; + data.tegan_local_user_view.local_user.show_bot_accounts = false; let mut read_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), ..data.default_post_query() } .list(&data.site, pool) @@ -1167,7 +1201,7 @@ mod tests { assert_eq!(vec![expected_post_with_upvote], read_post_listing); let like_removed = - PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id).await?; + PostLike::remove(pool, data.tegan_local_user_view.person.id, data.post.id).await?; assert_eq!(uplete::Count::only_deleted(1), like_removed); Ok(()) } @@ -1181,17 +1215,16 @@ mod tests { // Like both the bot post, and your own // The liked_only should not show your own post - let post_like_form = - PostLikeForm::new(data.inserted_post.id, data.local_user_view.person.id, 1); + let post_like_form = PostLikeForm::new(data.post.id, data.tegan_local_user_view.person.id, 1); PostLike::like(pool, &post_like_form).await?; let bot_post_like_form = - PostLikeForm::new(data.inserted_bot_post.id, data.local_user_view.person.id, 1); + PostLikeForm::new(data.bot_post.id, data.tegan_local_user_view.person.id, 1); PostLike::like(pool, &bot_post_like_form).await?; // Read the liked only let read_liked_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), liked_only: Some(true), ..data.default_post_query() } @@ -1202,7 +1235,7 @@ mod tests { assert_eq!(vec![POST_BY_BOT], names(&read_liked_post_listing)); let read_disliked_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), disliked_only: Some(true), ..data.default_post_query() } @@ -1224,13 +1257,12 @@ mod tests { // Only mark the bot post as read // The read_only should only show the bot post - let post_read_form = - PostReadForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); + let post_read_form = PostReadForm::new(data.bot_post.id, data.tegan_local_user_view.person.id); PostRead::mark_as_read(pool, &post_read_form).await?; // Only read the post marked as read let read_read_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), read_only: Some(true), ..data.default_post_query() } @@ -1249,33 +1281,128 @@ mod tests { async fn creator_info(data: &mut Data) -> LemmyResult<()> { let pool = &data.pool(); let pool = &mut pool.into(); + let community_id = data.community.id; - // Make one of the inserted persons a moderator - let person_id = data.local_user_view.person.id; - let community_id = data.inserted_community.id; - let form = CommunityModeratorForm { - community_id, - person_id, - }; - CommunityModerator::join(pool, &form).await?; - - let post_listing = PostQuery { - community_id: Some(data.inserted_community.id), + let tegan_listings = PostQuery { + community_id: Some(community_id), ..data.default_post_query() } .list(&data.site, pool) .await? .into_iter() - .map(|p| (p.creator.name, p.creator_is_moderator, p.creator_is_admin)) + .map(|p| { + ( + p.creator.name, + p.creator_is_moderator, + p.creator_is_admin, + p.can_mod, + ) + }) + .collect::>(); + + // Tegan is an admin, so can_mod should be always true + let expected_post_listing = vec![ + ("tegan".to_owned(), false, true, true), + ("mybot".to_owned(), false, false, true), + ("tegan".to_owned(), false, true, true), + ]; + assert_eq!(expected_post_listing, tegan_listings); + + // Have john become a moderator, then the bot + let john_mod_form = CommunityModeratorForm { + community_id, + person_id: data.john_local_user_view.person.id, + }; + CommunityModerator::join(pool, &john_mod_form).await?; + + let bot_mod_form = CommunityModeratorForm { + community_id, + person_id: data.bot_local_user_view.person.id, + }; + CommunityModerator::join(pool, &bot_mod_form).await?; + + let john_listings = PostQuery { + sort: Some(PostSortType::New), + local_user: Some(&data.john_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await? + .into_iter() + .map(|p| { + ( + p.creator.name, + p.creator_is_moderator, + p.creator_is_admin, + p.can_mod, + ) + }) + .collect::>(); + + // John is a mod, so he can_mod the bots (and his own) posts, but not tegans. + let expected_post_listing = vec![ + ("tegan".to_owned(), false, true, false), + ("mybot".to_owned(), true, false, true), + ("tegan".to_owned(), false, true, false), + ("john".to_owned(), true, false, true), + ]; + assert_eq!(expected_post_listing, john_listings); + + // Bot is also a mod, but was added after john, so can't mod anything + let bot_listings = PostQuery { + sort: Some(PostSortType::New), + local_user: Some(&data.bot_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await? + .into_iter() + .map(|p| { + ( + p.creator.name, + p.creator_is_moderator, + p.creator_is_admin, + p.can_mod, + ) + }) .collect::>(); let expected_post_listing = vec![ - ("tegan".to_owned(), true, true), - ("mybot".to_owned(), false, false), - ("tegan".to_owned(), true, true), + ("tegan".to_owned(), false, true, false), + ("mybot".to_owned(), true, false, true), + ("tegan".to_owned(), false, true, false), + ("john".to_owned(), true, false, false), ]; + assert_eq!(expected_post_listing, bot_listings); - assert_eq!(expected_post_listing, post_listing); + // Make the bot leave the mod team, and make sure it can_mod is false. + CommunityModerator::leave(pool, &bot_mod_form).await?; + + let bot_listings = PostQuery { + sort: Some(PostSortType::New), + local_user: Some(&data.bot_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await? + .into_iter() + .map(|p| { + ( + p.creator.name, + p.creator_is_moderator, + p.creator_is_admin, + p.can_mod, + ) + }) + .collect::>(); + + let expected_post_listing = vec![ + ("tegan".to_owned(), false, true, false), + ("mybot".to_owned(), false, false, false), + ("tegan".to_owned(), false, true, false), + ("john".to_owned(), true, false, false), + ]; + assert_eq!(expected_post_listing, bot_listings); Ok(()) } @@ -1297,8 +1424,8 @@ mod tests { language_id: Some(spanish_id), ..PostInsertForm::new( EL_POSTO.to_string(), - data.local_user_view.person.id, - data.inserted_community.id, + data.tegan_local_user_view.person.id, + data.community.id, ) }; Post::create(pool, &post_spanish).await?; @@ -1311,24 +1438,26 @@ mod tests { names(&post_listings_all) ); - LocalUserLanguage::update(pool, vec![french_id], data.local_user_view.local_user.id).await?; + LocalUserLanguage::update( + pool, + vec![french_id], + data.tegan_local_user_view.local_user.id, + ) + .await?; let post_listing_french = data.default_post_query().list(&data.site, pool).await?; // only one post in french and one undetermined should be returned - assert_eq!( - vec![POST_WITH_TAGS, POST_BY_BOT, POST], - names(&post_listing_french) - ); + assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listing_french)); assert_eq!( Some(french_id), - post_listing_french.get(2).map(|p| p.post.language_id) + post_listing_french.get(1).map(|p| p.post.language_id) ); LocalUserLanguage::update( pool, vec![french_id, UNDETERMINED_ID], - data.local_user_view.local_user.id, + data.tegan_local_user_view.local_user.id, ) .await?; let post_listings_french_und = data @@ -1360,7 +1489,7 @@ mod tests { // Remove the post Post::update( pool, - data.inserted_bot_post.id, + data.bot_post.id, &PostUpdateForm { removed: Some(true), ..Default::default() @@ -1373,9 +1502,9 @@ mod tests { assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listings_no_admin)); // Removed bot post is shown to admins on its profile page - data.local_user_view.local_user.admin = true; + data.tegan_local_user_view.local_user.admin = true; let post_listings_is_admin = PostQuery { - creator_id: Some(data.inserted_bot.id), + creator_id: Some(data.bot_local_user_view.person.id), ..data.default_post_query() } .list(&data.site, pool) @@ -1395,7 +1524,7 @@ mod tests { // Delete the post Post::update( pool, - data.inserted_post.id, + data.post.id, &PostUpdateForm { deleted: Some(true), ..Default::default() @@ -1406,8 +1535,8 @@ mod tests { // Deleted post is only shown to creator for (local_user, expect_contains_deleted) in [ (None, false), - (Some(&data.blocked_local_user_view.local_user), false), - (Some(&data.local_user_view.local_user), true), + (Some(&data.john_local_user_view.local_user), false), + (Some(&data.tegan_local_user_view.local_user), true), ] { let contains_deleted = PostQuery { local_user, @@ -1416,7 +1545,7 @@ mod tests { .list(&data.site, pool) .await? .iter() - .any(|p| p.post.id == data.inserted_post.id); + .any(|p| p.post.id == data.post.id); assert_eq!(expect_contains_deleted, contains_deleted); } @@ -1433,7 +1562,7 @@ mod tests { Community::update( pool, - data.inserted_community.id, + data.community.id, &CommunityUpdateForm { hidden: Some(true), ..Default::default() @@ -1450,7 +1579,7 @@ mod tests { // Follow the community let form = CommunityFollowerForm { state: Some(CommunityFollowerState::Accepted), - ..CommunityFollowerForm::new(data.inserted_community.id, data.local_user_view.person.id) + ..CommunityFollowerForm::new(data.community.id, data.tegan_local_user_view.person.id) }; CommunityFollower::follow(pool, &form).await?; @@ -1465,6 +1594,12 @@ mod tests { #[serial] async fn post_listing_instance_block(data: &mut Data) -> LemmyResult<()> { const POST_FROM_BLOCKED_INSTANCE: &str = "post on blocked instance"; + const POST_LISTING_WITH_BLOCKED: [&str; 4] = [ + POST_FROM_BLOCKED_INSTANCE, + POST_WITH_TAGS, + POST_BY_BOT, + POST, + ]; let pool = &data.pool(); let pool = &mut pool.into(); @@ -1483,7 +1618,7 @@ mod tests { language_id: Some(LanguageId(1)), ..PostInsertForm::new( POST_FROM_BLOCKED_INSTANCE.to_string(), - data.inserted_bot.id, + data.bot_local_user_view.person.id, inserted_community.id, ) }; @@ -1491,19 +1626,11 @@ mod tests { // no instance block, should return all posts let post_listings_all = data.default_post_query().list(&data.site, pool).await?; - assert_eq!( - vec![ - POST_FROM_BLOCKED_INSTANCE, - POST_WITH_TAGS, - POST_BY_BOT, - POST - ], - names(&post_listings_all) - ); + assert_eq!(POST_LISTING_WITH_BLOCKED, *names(&post_listings_all)); // block the instance let block_form = InstanceBlockForm { - person_id: data.local_user_view.person.id, + person_id: data.tegan_local_user_view.person.id, instance_id: blocked_instance.id, }; InstanceBlock::block(pool, &block_form).await?; @@ -1518,18 +1645,19 @@ mod tests { .iter() .all(|p| p.post.id != post_from_blocked_instance.id)); + // Follow community from the blocked instance to see posts anyway + let mut follow_form = + CommunityFollowerForm::new(inserted_community.id, data.tegan_local_user_view.person.id); + follow_form.state = Some(CommunityFollowerState::Accepted); + CommunityFollower::follow(pool, &follow_form).await?; + let post_listings_bypass = data.default_post_query().list(&data.site, pool).await?; + assert_eq!(POST_LISTING_WITH_BLOCKED, *names(&post_listings_bypass)); + CommunityFollower::unfollow(pool, &follow_form).await?; + // after unblocking it should return all posts again InstanceBlock::unblock(pool, &block_form).await?; let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; - assert_eq!( - vec![ - POST_FROM_BLOCKED_INSTANCE, - POST_WITH_TAGS, - POST_BY_BOT, - POST - ], - names(&post_listings_blocked) - ); + assert_eq!(POST_LISTING_WITH_BLOCKED, *names(&post_listings_blocked)); Instance::delete(pool, blocked_instance.id).await?; Ok(()) @@ -1543,7 +1671,7 @@ mod tests { let pool = &mut pool.into(); let community_form = CommunityInsertForm::new( - data.inserted_instance.id, + data.instance.id, "yes".to_string(), "yes".to_owned(), "pubkey".to_string(), @@ -1563,7 +1691,7 @@ mod tests { published: Some(Utc::now() - Duration::from_secs(comments % 3)), ..PostInsertForm::new( "keep Christ in Christmas".to_owned(), - data.local_user_view.person.id, + data.tegan_local_user_view.person.id, inserted_community.id, ) }; @@ -1572,7 +1700,7 @@ mod tests { for _ in 0..comments { let comment_form = CommentInsertForm::new( - data.local_user_view.person.id, + data.tegan_local_user_view.person.id, inserted_post.id, "yes".to_owned(), ); @@ -1594,7 +1722,7 @@ mod tests { loop { let post_listings = PostQuery { page_after: page_after.map(|p| PaginationCursorData { - post_aggregates: p, + post: p, post_actions: Default::default(), }), ..options.clone() @@ -1604,8 +1732,8 @@ mod tests { listed_post_ids.extend(post_listings.iter().map(|p| p.post.id)); - if let Some(p) = post_listings.into_iter().last() { - page_after = Some(p.counts); + if let Some(p) = post_listings.into_iter().next_back() { + page_after = Some(p.post); } else { break; } @@ -1617,7 +1745,7 @@ mod tests { loop { let post_listings = PostQuery { page_after: page_before.map(|p| PaginationCursorData { - post_aggregates: p, + post: p, post_actions: Default::default(), }), page_back: Some(true), @@ -1636,7 +1764,7 @@ mod tests { listed_post_ids_forward.truncate(index); if let Some(p) = post_listings.into_iter().next() { - page_before = Some(p.counts); + page_before = Some(p.post); } else { break; } @@ -1663,11 +1791,16 @@ mod tests { show_read_posts: Some(false), ..Default::default() }; - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user.show_read_posts = false; + LocalUser::update( + pool, + data.tegan_local_user_view.local_user.id, + &local_user_form, + ) + .await?; + data.tegan_local_user_view.local_user.show_read_posts = false; // Mark a post as read - let read_form = PostReadForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); + let read_form = PostReadForm::new(data.bot_post.id, data.tegan_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 @@ -1708,12 +1841,7 @@ mod tests { let pool = &mut pool.into(); // Mark a post as hidden - PostHide::hide( - pool, - data.inserted_bot_post.id, - data.local_user_view.person.id, - ) - .await?; + PostHide::hide(pool, data.bot_post.id, data.tegan_local_user_view.person.id).await?; // Make sure you don't see the hidden post in the results let post_listings_hide_hidden = data.default_post_query().list(&data.site, pool).await?; @@ -1725,7 +1853,7 @@ mod tests { // Make sure it does come back with the show_hidden option let post_listings_show_hidden = PostQuery { sort: Some(PostSortType::New), - local_user: Some(&data.local_user_view.local_user), + local_user: Some(&data.tegan_local_user_view.local_user), show_hidden: Some(true), ..Default::default() } @@ -1755,7 +1883,7 @@ mod tests { ..Default::default() }; - Post::update(pool, data.inserted_post_with_tags.id, &update_form).await?; + Post::update(pool, data.post_with_tags.id, &update_form).await?; // Make sure you don't see the nsfw post in the regular results let post_listings_hide_nsfw = data.default_post_query().list(&data.site, pool).await?; @@ -1765,7 +1893,7 @@ mod tests { let post_listings_show_nsfw = PostQuery { sort: Some(PostSortType::New), show_nsfw: Some(true), - local_user: Some(&data.local_user_view.local_user), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) @@ -1787,13 +1915,12 @@ mod tests { Ok(()) } - async fn expected_post_view(data: &Data, pool: &mut DbPool<'_>) -> LemmyResult { + fn expected_post_view(data: &Data) -> LemmyResult { let (inserted_person, inserted_community, inserted_post) = ( - &data.local_user_view.person, - &data.inserted_community, - &data.inserted_post, + &data.tegan_local_user_view.person, + &data.community, + &data.post, ); - let agg = PostAggregates::read(pool, inserted_post.id).await?; Ok(PostView { post: Post { @@ -1821,6 +1948,19 @@ mod tests { featured_local: false, url_content_type: None, scheduled_publish_time: None, + comments: 0, + score: 0, + upvotes: 0, + downvotes: 0, + newest_comment_time_necro: inserted_post.published, + newest_comment_time: inserted_post.published, + hot_rank: RANK_DEFAULT, + hot_rank_active: RANK_DEFAULT, + controversy_rank: 0.0, + scaled_rank: RANK_DEFAULT, + instance_id: data.instance.id, + report_count: 0, + unresolved_report_count: 0, }, my_vote: None, unread_comments: 0, @@ -1830,7 +1970,7 @@ mod tests { display_name: None, published: inserted_person.published, avatar: None, - actor_id: inserted_person.actor_id.clone(), + ap_id: inserted_person.ap_id.clone(), local: true, bot_account: false, banned: false, @@ -1841,16 +1981,21 @@ mod tests { inbox_url: inserted_person.inbox_url.clone(), matrix_user_id: None, ban_expires: None, - instance_id: data.inserted_instance.id, + instance_id: data.instance.id, private_key: inserted_person.private_key.clone(), public_key: inserted_person.public_key.clone(), last_refreshed_at: inserted_person.last_refreshed_at, + post_count: 2, + post_score: 0, + comment_count: 0, + comment_score: 0, }, image_details: None, creator_banned_from_community: false, banned_from_community: false, creator_is_moderator: false, creator_is_admin: true, + can_mod: true, community: Community { id: inserted_community.id, name: inserted_community.name.clone(), @@ -1858,7 +2003,7 @@ mod tests { removed: false, deleted: false, nsfw: false, - actor_id: inserted_community.actor_id.clone(), + ap_id: inserted_community.ap_id.clone(), local: true, title: "nada".to_owned(), sidebar: None, @@ -1868,7 +2013,7 @@ mod tests { hidden: false, posting_restricted_to_mods: false, published: inserted_community.published, - instance_id: data.inserted_instance.id, + instance_id: data.instance.id, private_key: inserted_community.private_key.clone(), public_key: inserted_community.public_key.clone(), last_refreshed_at: inserted_community.last_refreshed_at, @@ -1878,32 +2023,23 @@ mod tests { featured_url: inserted_community.featured_url.clone(), visibility: CommunityVisibility::Public, random_number: inserted_community.random_number, - }, - counts: PostAggregates { - post_id: inserted_post.id, + subscribers: 0, + posts: 4, comments: 0, - score: 0, - upvotes: 0, - downvotes: 0, - published: agg.published, - newest_comment_time_necro: inserted_post.published, - newest_comment_time: inserted_post.published, - featured_community: false, - featured_local: false, + users_active_day: 0, + users_active_week: 0, + users_active_month: 0, + users_active_half_year: 0, hot_rank: RANK_DEFAULT, - hot_rank_active: RANK_DEFAULT, - controversy_rank: 0.0, - scaled_rank: RANK_DEFAULT, - community_id: inserted_post.community_id, - creator_id: inserted_post.creator_id, - instance_id: data.inserted_instance.id, + subscribers_local: 0, report_count: 0, unresolved_report_count: 0, + interactions_month: 0, }, subscribed: SubscribedType::NotSubscribed, read: false, hidden: false, - saved: false, + saved: None, creator_blocked: false, tags: PostTags::default(), }) @@ -1918,7 +2054,7 @@ mod tests { Community::update( pool, - data.inserted_community.id, + data.community.id, &CommunityUpdateForm { visibility: Some(CommunityVisibility::LocalOnly), ..Default::default() @@ -1934,20 +2070,20 @@ mod tests { assert_eq!(0, unauthenticated_query.len()); let authenticated_query = PostQuery { - local_user: Some(&data.local_user_view.local_user), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) .await?; assert_eq!(3, authenticated_query.len()); - let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await; + let unauthenticated_post = PostView::read(pool, data.post.id, None, false).await; assert!(unauthenticated_post.is_err()); let authenticated_post = PostView::read( pool, - data.inserted_post.id, - Some(&data.local_user_view.local_user), + data.post.id, + Some(&data.tegan_local_user_view.local_user), false, ) .await; @@ -1964,7 +2100,7 @@ mod tests { let pool = &mut pool.into(); // Test that post view shows if local user is blocked from community - let banned_from_comm_person = PersonInsertForm::test_form(data.inserted_instance.id, "jill"); + let banned_from_comm_person = PersonInsertForm::test_form(data.instance.id, "jill"); let inserted_banned_from_comm_person = Person::create(pool, &banned_from_comm_person).await?; @@ -1978,7 +2114,7 @@ mod tests { CommunityPersonBan::ban( pool, &CommunityPersonBanForm { - community_id: data.inserted_community.id, + community_id: data.community.id, person_id: inserted_banned_from_comm_person.id, expires: None, }, @@ -1987,7 +2123,7 @@ mod tests { let post_view = PostView::read( pool, - data.inserted_post.id, + data.post.id, Some(&inserted_banned_from_comm_local_user), false, ) @@ -2008,8 +2144,8 @@ mod tests { let post_view = PostView::read( pool, - data.inserted_post.id, - Some(&data.local_user_view.local_user), + data.post.id, + Some(&data.tegan_local_user_view.local_user), false, ) .await?; @@ -2039,8 +2175,8 @@ mod tests { url, ..PostInsertForm::new( name, - data.local_user_view.person.id, - data.inserted_community.id, + data.tegan_local_user_view.person.id, + data.community.id, ) }; Post::create(pool, &post_form).await?; @@ -2056,7 +2192,7 @@ mod tests { let now = Instant::now(); PostQuery { sort: Some(PostSortType::Active), - local_user: Some(&data.local_user_view.local_user), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) @@ -2084,8 +2220,8 @@ mod tests { // Create a comment for a post let comment_form = CommentInsertForm::new( - data.local_user_view.person.id, - data.inserted_post.id, + data.tegan_local_user_view.person.id, + data.post.id, "a comment".to_owned(), ); Comment::create(pool, &comment_form, None).await?; @@ -2094,7 +2230,7 @@ mod tests { let post_listings_no_comments = PostQuery { sort: Some(PostSortType::New), no_comments_only: Some(true), - local_user: Some(&data.local_user_view.local_user), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) @@ -2118,7 +2254,7 @@ mod tests { // Mark community as private Community::update( pool, - data.inserted_community.id, + data.community.id, &CommunityUpdateForm { visibility: Some(CommunityVisibility::Private), ..Default::default() @@ -2128,20 +2264,20 @@ mod tests { // No posts returned without auth let read_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), + community_id: Some(data.community.id), ..Default::default() } .list(&data.site, pool) .await?; assert_eq!(0, read_post_listing.len()); - let post_view = PostView::read(pool, data.inserted_post.id, None, false).await; + let post_view = PostView::read(pool, data.post.id, None, false).await; assert!(post_view.is_err()); // No posts returned for non-follower who is not admin - data.local_user_view.local_user.admin = false; + data.tegan_local_user_view.local_user.admin = false; let read_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), - local_user: Some(&data.local_user_view.local_user), + community_id: Some(data.community.id), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) @@ -2149,18 +2285,18 @@ mod tests { assert_eq!(0, read_post_listing.len()); let post_view = PostView::read( pool, - data.inserted_post.id, - Some(&data.local_user_view.local_user), + data.post.id, + Some(&data.tegan_local_user_view.local_user), false, ) .await; assert!(post_view.is_err()); // Admin can view content without following - data.local_user_view.local_user.admin = true; + data.tegan_local_user_view.local_user.admin = true; let read_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), - local_user: Some(&data.local_user_view.local_user), + community_id: Some(data.community.id), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) @@ -2168,26 +2304,26 @@ mod tests { assert_eq!(3, read_post_listing.len()); let post_view = PostView::read( pool, - data.inserted_post.id, - Some(&data.local_user_view.local_user), + data.post.id, + Some(&data.tegan_local_user_view.local_user), true, ) .await; assert!(post_view.is_ok()); - data.local_user_view.local_user.admin = false; + data.tegan_local_user_view.local_user.admin = false; // User can view after following CommunityFollower::follow( pool, &CommunityFollowerForm { state: Some(CommunityFollowerState::Accepted), - ..CommunityFollowerForm::new(data.inserted_community.id, data.local_user_view.person.id) + ..CommunityFollowerForm::new(data.community.id, data.tegan_local_user_view.person.id) }, ) .await?; let read_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), - local_user: Some(&data.local_user_view.local_user), + community_id: Some(data.community.id), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) @@ -2195,8 +2331,8 @@ mod tests { assert_eq!(3, read_post_listing.len()); let post_view = PostView::read( pool, - data.inserted_post.id, - Some(&data.local_user_view.local_user), + data.post.id, + Some(&data.tegan_local_user_view.local_user), true, ) .await; @@ -2215,7 +2351,7 @@ mod tests { // Make one post an image post Post::update( pool, - data.inserted_bot_post.id, + data.bot_post.id, &PostUpdateForm { url_content_type: Some(Some(String::from("image/png"))), ..Default::default() @@ -2225,8 +2361,8 @@ mod tests { // Make sure all the posts are returned when `hide_media` is unset let hide_media_listing = PostQuery { - community_id: Some(data.inserted_community.id), - local_user: Some(&data.local_user_view.local_user), + community_id: Some(data.community.id), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) @@ -2238,13 +2374,18 @@ mod tests { hide_media: Some(true), ..Default::default() }; - LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; - data.local_user_view.local_user.hide_media = true; + LocalUser::update( + pool, + data.tegan_local_user_view.local_user.id, + &local_user_form, + ) + .await?; + data.tegan_local_user_view.local_user.hide_media = true; // Ensure you don't see the image post let hide_media_listing = PostQuery { - community_id: Some(data.inserted_community.id), - local_user: Some(&data.local_user_view.local_user), + community_id: Some(data.community.id), + local_user: Some(&data.tegan_local_user_view.local_user), ..Default::default() } .list(&data.site, pool) @@ -2253,8 +2394,8 @@ mod tests { // Make sure the `hide_media` override works let hide_media_listing = PostQuery { - community_id: Some(data.inserted_community.id), - local_user: Some(&data.local_user_view.local_user), + community_id: Some(data.community.id), + local_user: Some(&data.tegan_local_user_view.local_user), hide_media: Some(false), ..Default::default() } @@ -2274,8 +2415,8 @@ mod tests { let post_view = PostView::read( pool, - data.inserted_post_with_tags.id, - Some(&data.local_user_view.local_user), + data.post_with_tags.id, + Some(&data.tegan_local_user_view.local_user), false, ) .await?; diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs deleted file mode 100644 index 4c7fd676c..000000000 --- a/crates/db_views/src/post_report_view.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::structs::PostReportView; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - aliases::{self, creator_community_actions}, - newtypes::{PersonId, PostReportId}, - schema::{ - community, - community_actions, - local_user, - person, - person_actions, - post, - post_actions, - post_aggregates, - post_report, - }, - source::community::CommunityFollower, - utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, -}; - -impl PostReportView { - /// returns the PostReportView for the provided report_id - /// - /// * `report_id` - the report id to obtain - pub async fn read( - pool: &mut DbPool<'_>, - report_id: PostReportId, - my_person_id: PersonId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - - post_report::table - .find(report_id) - .inner_join(post::table) - .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(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 - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ) - .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 - .on(post_report::resolver_id.eq(aliases::person2.field(person::id).nullable())), - ) - .select(( - post_report::all_columns, - post::all_columns, - community::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - 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_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() - post_actions::read_comments_amount.nullable(), - post_aggregates::comments, - ), - post_aggregates::all_columns, - aliases::person2.fields(person::all_columns.nullable()), - )) - .first(conn) - .await - } -} diff --git a/crates/db_views/src/private_message/mod.rs b/crates/db_views/src/private_message/mod.rs new file mode 100644 index 000000000..71bde5dd1 --- /dev/null +++ b/crates/db_views/src/private_message/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "full")] +pub mod private_message_view; diff --git a/crates/db_views/src/private_message/private_message_view.rs b/crates/db_views/src/private_message/private_message_view.rs new file mode 100644 index 000000000..799fdde82 --- /dev/null +++ b/crates/db_views/src/private_message/private_message_view.rs @@ -0,0 +1,57 @@ +use crate::structs::PrivateMessageView; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases, + newtypes::PrivateMessageId, + schema::{instance_actions, person, person_actions, private_message}, + utils::{get_conn, DbPool}, +}; + +impl PrivateMessageView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins() -> _ { + let recipient_id = aliases::person1.field(person::id); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(private_message::creator_id) + .and(person_actions::person_id.eq(recipient_id)), + ); + + let instance_actions_join = instance_actions::table.on( + instance_actions::instance_id + .eq(person::instance_id) + .and(instance_actions::person_id.eq(recipient_id)), + ); + + let creator_join = person::table.on(private_message::creator_id.eq(person::id)); + + let recipient_join = aliases::person1.on(private_message::recipient_id.eq(recipient_id)); + + private_message::table + .inner_join(creator_join) + .inner_join(recipient_join) + .left_join(person_actions_join) + .left_join(instance_actions_join) + } + + pub async fn read( + pool: &mut DbPool<'_>, + private_message_id: PrivateMessageId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(private_message::id.eq(private_message_id)) + .select(Self::as_select()) + .first(conn) + .await + } +} diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs deleted file mode 100644 index 346dab49a..000000000 --- a/crates/db_views/src/private_message_view.rs +++ /dev/null @@ -1,396 +0,0 @@ -use crate::structs::PrivateMessageView; -use diesel::{ - debug_query, - pg::Pg, - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - aliases, - newtypes::{PersonId, PrivateMessageId}, - schema::{instance_actions, person, person_actions, private_message}, - utils::{actions, get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, -}; -use tracing::debug; - -fn queries<'a>() -> Queries< - impl ReadFn<'a, PrivateMessageView, PrivateMessageId>, - impl ListFn<'a, PrivateMessageView, (PrivateMessageQuery, PersonId)>, -> { - let all_joins = |query: private_message::BoxedQuery<'a, Pg>| { - query - .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .inner_join( - aliases::person1.on(private_message::recipient_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 = ( - private_message::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - ); - - let read = move |mut conn: DbConn<'a>, private_message_id: PrivateMessageId| async move { - all_joins(private_message::table.find(private_message_id).into_boxed()) - .order_by(private_message::published.desc()) - .select(selection) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, (o, recipient_id): (PrivateMessageQuery, PersonId)| async move { - let mut query = all_joins(private_message::table.into_boxed()) - .select(selection) - // Dont show replies from blocked users - .filter(person_actions::blocked.is_null()) - // Dont show replies from blocked instances - .filter(instance_actions::blocked.is_null()); - - // If its unread, I only want the ones to me - if o.unread_only { - query = query.filter(private_message::read.eq(false)); - if let Some(i) = o.creator_id { - query = query.filter(private_message::creator_id.eq(i)) - } - query = query.filter(private_message::recipient_id.eq(recipient_id)); - } - // Otherwise, I want the ALL view to show both sent and received - else { - query = query.filter( - private_message::recipient_id - .eq(recipient_id) - .or(private_message::creator_id.eq(recipient_id)), - ); - if let Some(i) = o.creator_id { - query = query.filter( - private_message::creator_id - .eq(i) - .or(private_message::recipient_id.eq(i)), - ) - } - } - - let (limit, offset) = limit_and_offset(o.page, o.limit)?; - - query = query - .filter(private_message::deleted.eq(false)) - .limit(limit) - .offset(offset) - .order_by(private_message::published.desc()); - - debug!( - "Private Message View Query: {:?}", - debug_query::(&query) - ); - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl PrivateMessageView { - pub async fn read( - pool: &mut DbPool<'_>, - private_message_id: PrivateMessageId, - ) -> Result { - queries().read(pool, private_message_id).await - } - - /// Gets the number of unread messages - pub async fn get_unread_messages( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - ) -> Result { - use diesel::dsl::count; - let conn = &mut get_conn(pool).await?; - private_message::table - // Necessary to get the senders instance_id - .inner_join(person::table.on(private_message::creator_id.eq(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_actions::blocked.is_null()) - // Dont count replies from blocked instances - .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)) - .select(count(private_message::id)) - .first::(conn) - .await - } -} - -#[derive(Default)] -pub struct PrivateMessageQuery { - pub unread_only: bool, - pub page: Option, - pub limit: Option, - pub creator_id: Option, -} - -impl PrivateMessageQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - recipient_id: PersonId, - ) -> Result, Error> { - queries().list(pool, (self, recipient_id)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView}; - use lemmy_db_schema::{ - assert_length, - newtypes::InstanceId, - source::{ - instance::Instance, - instance_block::{InstanceBlock, InstanceBlockForm}, - person::{Person, PersonInsertForm}, - person_block::{PersonBlock, PersonBlockForm}, - private_message::{PrivateMessage, PrivateMessageInsertForm}, - }, - traits::{Blockable, Crud}, - utils::{build_db_pool_for_tests, DbPool}, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - struct Data { - instance: Instance, - timmy: Person, - jess: Person, - sara: Person, - } - - async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { - let message_content = String::new(); - - let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_rav"); - - let timmy = Person::create(pool, &timmy_form).await?; - - let sara_form = PersonInsertForm::test_form(instance.id, "sara_rav"); - - let sara = Person::create(pool, &sara_form).await?; - - let jess_form = PersonInsertForm::test_form(instance.id, "jess_rav"); - - let jess = Person::create(pool, &jess_form).await?; - - let sara_timmy_message_form = - PrivateMessageInsertForm::new(sara.id, timmy.id, message_content.clone()); - PrivateMessage::create(pool, &sara_timmy_message_form).await?; - - let sara_jess_message_form = - PrivateMessageInsertForm::new(sara.id, jess.id, message_content.clone()); - PrivateMessage::create(pool, &sara_jess_message_form).await?; - - let timmy_sara_message_form = - PrivateMessageInsertForm::new(timmy.id, sara.id, message_content.clone()); - PrivateMessage::create(pool, &timmy_sara_message_form).await?; - - let jess_timmy_message_form = - PrivateMessageInsertForm::new(jess.id, timmy.id, message_content.clone()); - PrivateMessage::create(pool, &jess_timmy_message_form).await?; - - Ok(Data { - instance, - timmy, - jess, - sara, - }) - } - - async fn cleanup(instance_id: InstanceId, pool: &mut DbPool<'_>) -> LemmyResult<()> { - // This also deletes all persons and private messages thanks to sql `on delete cascade` - Instance::delete(pool, instance_id).await?; - Ok(()) - } - - #[tokio::test] - #[serial] - async fn read_private_messages() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let Data { - timmy, - jess, - sara, - instance, - } = init_data(pool).await?; - - let timmy_messages = PrivateMessageQuery { - unread_only: false, - creator_id: None, - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(3, &timmy_messages); - assert_eq!(timmy_messages[0].creator.id, jess.id); - assert_eq!(timmy_messages[0].recipient.id, timmy.id); - assert_eq!(timmy_messages[1].creator.id, timmy.id); - assert_eq!(timmy_messages[1].recipient.id, sara.id); - assert_eq!(timmy_messages[2].creator.id, sara.id); - assert_eq!(timmy_messages[2].recipient.id, timmy.id); - - let timmy_unread_messages = PrivateMessageQuery { - unread_only: true, - creator_id: None, - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(2, &timmy_unread_messages); - assert_eq!(timmy_unread_messages[0].creator.id, jess.id); - assert_eq!(timmy_unread_messages[0].recipient.id, timmy.id); - assert_eq!(timmy_unread_messages[1].creator.id, sara.id); - assert_eq!(timmy_unread_messages[1].recipient.id, timmy.id); - - let timmy_sara_messages = PrivateMessageQuery { - unread_only: false, - creator_id: Some(sara.id), - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(2, &timmy_sara_messages); - assert_eq!(timmy_sara_messages[0].creator.id, timmy.id); - assert_eq!(timmy_sara_messages[0].recipient.id, sara.id); - assert_eq!(timmy_sara_messages[1].creator.id, sara.id); - assert_eq!(timmy_sara_messages[1].recipient.id, timmy.id); - - let timmy_sara_unread_messages = PrivateMessageQuery { - unread_only: true, - creator_id: Some(sara.id), - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(1, &timmy_sara_unread_messages); - assert_eq!(timmy_sara_unread_messages[0].creator.id, sara.id); - assert_eq!(timmy_sara_unread_messages[0].recipient.id, timmy.id); - - cleanup(instance.id, pool).await - } - - #[tokio::test] - #[serial] - async fn ensure_person_block() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let Data { - timmy, - sara, - instance, - jess: _, - } = init_data(pool).await?; - - // Make sure blocks are working - let timmy_blocks_sara_form = PersonBlockForm { - person_id: timmy.id, - target_id: sara.id, - }; - - let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form).await?; - - let expected_block = PersonBlock { - person_id: timmy.id, - target_id: sara.id, - published: inserted_block.published, - }; - assert_eq!(expected_block, inserted_block); - - let timmy_messages = PrivateMessageQuery { - unread_only: true, - creator_id: None, - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(1, &timmy_messages); - - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; - assert_eq!(timmy_unread_messages, 1); - - cleanup(instance.id, pool).await - } - - #[tokio::test] - #[serial] - async fn ensure_instance_block() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let Data { - timmy, - jess: _, - sara, - instance, - } = init_data(pool).await?; - // Make sure instance_blocks are working - let timmy_blocks_instance_form = InstanceBlockForm { - person_id: timmy.id, - instance_id: sara.instance_id, - }; - - let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form).await?; - - let expected_instance_block = InstanceBlock { - person_id: timmy.id, - instance_id: sara.instance_id, - published: inserted_instance_block.published, - }; - assert_eq!(expected_instance_block, inserted_instance_block); - - let timmy_messages = PrivateMessageQuery { - unread_only: true, - creator_id: None, - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(0, &timmy_messages); - - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; - assert_eq!(timmy_unread_messages, 0); - cleanup(instance.id, pool).await - } -} diff --git a/crates/db_views/src/registration_applications/mod.rs b/crates/db_views/src/registration_applications/mod.rs new file mode 100644 index 000000000..b71cca970 --- /dev/null +++ b/crates/db_views/src/registration_applications/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "full")] +pub mod registration_application_view; diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_applications/registration_application_view.rs similarity index 50% rename from crates/db_views/src/registration_application_view.rs rename to crates/db_views/src/registration_applications/registration_application_view.rs index 87bbd5b01..f3bac8f71 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_applications/registration_application_view.rs @@ -1,119 +1,69 @@ use crate::structs::RegistrationApplicationView; use diesel::{ dsl::count, - pg::Pg, result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl, + SelectableHelper, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, newtypes::{PersonId, RegistrationApplicationId}, schema::{local_user, person, registration_application}, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + source::registration_application::RegistrationApplication, + utils::{get_conn, limit_and_offset, DbPool}, }; -enum ReadBy { - Id(RegistrationApplicationId), - Person(PersonId), -} - -fn queries<'a>() -> Queries< - impl ReadFn<'a, RegistrationApplicationView, ReadBy>, - impl ListFn<'a, RegistrationApplicationView, RegistrationApplicationQuery>, -> { - let all_joins = |query: registration_application::BoxedQuery<'a, Pg>| { - query +impl RegistrationApplicationView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins() -> _ { + registration_application::table .inner_join(local_user::table.on(registration_application::local_user_id.eq(local_user::id))) .inner_join(person::table.on(local_user::person_id.eq(person::id))) .left_join( aliases::person1 .on(registration_application::admin_id.eq(aliases::person1.field(person::id).nullable())), ) - .order_by(registration_application::published.desc()) - .select(( - registration_application::all_columns, - local_user::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns).nullable(), - )) - }; + } - let read = move |mut conn: DbConn<'a>, search: ReadBy| async move { - let mut query = all_joins(registration_application::table.into_boxed()); - - query = match search { - ReadBy::Id(id) => query.filter(registration_application::id.eq(id)), - ReadBy::Person(person_id) => query.filter(person::id.eq(person_id)), - }; - - query.first(&mut conn).await - }; - - let list = move |mut conn: DbConn<'a>, o: RegistrationApplicationQuery| async move { - let mut query = all_joins(registration_application::table.into_boxed()); - - // If viewing all applications, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if o.unread_only { - query = query - .filter(registration_application::admin_id.is_null()) - .order_by(registration_application::published.asc()); - } else { - query = query.order_by(registration_application::published.desc()); - } - - if o.verified_email_only { - query = query.filter(local_user::email_verified.eq(true)) - } - - let (limit, offset) = limit_and_offset(o.page, o.limit)?; - - query = query.limit(limit).offset(offset); - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl RegistrationApplicationView { pub async fn read(pool: &mut DbPool<'_>, id: RegistrationApplicationId) -> Result { - queries().read(pool, ReadBy::Id(id)).await + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(registration_application::id.eq(id)) + .select(Self::as_select()) + .first(conn) + .await } pub async fn read_by_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { - queries().read(pool, ReadBy::Person(person_id)).await + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(person::id.eq(person_id)) + .select(Self::as_select()) + .first(conn) + .await } + /// Returns the current unread registration_application count pub async fn get_unread_count( pool: &mut DbPool<'_>, verified_email_only: bool, ) -> Result { let conn = &mut get_conn(pool).await?; - let person_alias_1 = diesel::alias!(person as person1); - let mut query = registration_application::table - .inner_join(local_user::table.on(registration_application::local_user_id.eq(local_user::id))) - .inner_join(person::table.on(local_user::person_id.eq(person::id))) - .left_join( - person_alias_1 - .on(registration_application::admin_id.eq(person_alias_1.field(person::id).nullable())), - ) - .filter(registration_application::admin_id.is_null()) + let mut query = Self::joins() + .filter(RegistrationApplication::is_unread()) + .select(count(registration_application::id)) .into_boxed(); if verified_email_only { query = query.filter(local_user::email_verified.eq(true)) } - query - .select(count(registration_application::id)) - .first::(conn) - .await + query.first::(conn).await } } @@ -130,14 +80,39 @@ impl RegistrationApplicationQuery { self, pool: &mut DbPool<'_>, ) -> Result, Error> { - queries().list(pool, self).await + let conn = &mut get_conn(pool).await?; + let o = self; + + let mut query = RegistrationApplicationView::joins() + .select(RegistrationApplicationView::as_select()) + .into_boxed(); + + if o.unread_only { + query = query + .filter(RegistrationApplication::is_unread()) + .order_by(registration_application::published.asc()); + } else { + query = query.order_by(registration_application::published.desc()); + } + + if o.verified_email_only { + query = query.filter(local_user::email_verified.eq(true)) + } + + let (limit, offset) = limit_and_offset(o.page, o.limit)?; + + query + .limit(limit) + .offset(offset) + .load::(conn) + .await } } #[cfg(test)] mod tests { - use crate::registration_application_view::{ + use crate::registration_applications::registration_application_view::{ RegistrationApplicationQuery, RegistrationApplicationView, }; @@ -165,28 +140,28 @@ mod tests { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rav"); + let timmy_person_form = PersonInsertForm::test_form(instance.id, "timmy_rav"); - let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?; + let timmy_person = Person::create(pool, &timmy_person_form).await?; - let timmy_local_user_form = LocalUserInsertForm::test_form_admin(inserted_timmy_person.id); + let timmy_local_user_form = LocalUserInsertForm::test_form_admin(timmy_person.id); let _inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; - let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rav"); + let sara_person_form = PersonInsertForm::test_form(instance.id, "sara_rav"); - let inserted_sara_person = Person::create(pool, &sara_person_form).await?; + let sara_person = Person::create(pool, &sara_person_form).await?; - let sara_local_user_form = LocalUserInsertForm::test_form(inserted_sara_person.id); + let sara_local_user_form = LocalUserInsertForm::test_form(sara_person.id); - let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]).await?; + let sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]).await?; // Sara creates an application let sara_app_form = RegistrationApplicationInsertForm { - local_user_id: inserted_sara_local_user.id, + local_user_id: sara_local_user.id, answer: "LET ME IIIIINN".to_string(), }; @@ -194,17 +169,17 @@ mod tests { let read_sara_app_view = RegistrationApplicationView::read(pool, sara_app.id).await?; - let jess_person_form = PersonInsertForm::test_form(inserted_instance.id, "jess_rav"); + let jess_person_form = PersonInsertForm::test_form(instance.id, "jess_rav"); let inserted_jess_person = Person::create(pool, &jess_person_form).await?; let jess_local_user_form = LocalUserInsertForm::test_form(inserted_jess_person.id); - let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]).await?; + let jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]).await?; // Sara creates an application let jess_app_form = RegistrationApplicationInsertForm { - local_user_id: inserted_jess_local_user.id, + local_user_id: jess_local_user.id, answer: "LET ME IIIIINN".to_string(), }; @@ -215,42 +190,44 @@ mod tests { let mut expected_sara_app_view = RegistrationApplicationView { registration_application: sara_app.clone(), creator_local_user: LocalUser { - id: inserted_sara_local_user.id, - person_id: inserted_sara_local_user.person_id, - email: inserted_sara_local_user.email, - show_nsfw: inserted_sara_local_user.show_nsfw, - blur_nsfw: inserted_sara_local_user.blur_nsfw, - theme: inserted_sara_local_user.theme, - default_post_sort_type: inserted_sara_local_user.default_post_sort_type, - default_comment_sort_type: inserted_sara_local_user.default_comment_sort_type, - default_listing_type: inserted_sara_local_user.default_listing_type, - interface_language: inserted_sara_local_user.interface_language, - show_avatars: inserted_sara_local_user.show_avatars, - send_notifications_to_email: inserted_sara_local_user.send_notifications_to_email, - show_bot_accounts: inserted_sara_local_user.show_bot_accounts, - show_read_posts: inserted_sara_local_user.show_read_posts, - email_verified: inserted_sara_local_user.email_verified, - accepted_application: inserted_sara_local_user.accepted_application, - totp_2fa_secret: inserted_sara_local_user.totp_2fa_secret, - password_encrypted: inserted_sara_local_user.password_encrypted, - open_links_in_new_tab: inserted_sara_local_user.open_links_in_new_tab, - infinite_scroll_enabled: inserted_sara_local_user.infinite_scroll_enabled, - post_listing_mode: inserted_sara_local_user.post_listing_mode, - totp_2fa_enabled: inserted_sara_local_user.totp_2fa_enabled, - enable_keyboard_navigation: inserted_sara_local_user.enable_keyboard_navigation, - enable_animated_images: inserted_sara_local_user.enable_animated_images, - enable_private_messages: inserted_sara_local_user.enable_private_messages, - collapse_bot_comments: inserted_sara_local_user.collapse_bot_comments, - last_donation_notification: inserted_sara_local_user.last_donation_notification, + id: sara_local_user.id, + person_id: sara_local_user.person_id, + email: sara_local_user.email, + show_nsfw: sara_local_user.show_nsfw, + blur_nsfw: sara_local_user.blur_nsfw, + theme: sara_local_user.theme, + default_post_sort_type: sara_local_user.default_post_sort_type, + default_comment_sort_type: sara_local_user.default_comment_sort_type, + default_listing_type: sara_local_user.default_listing_type, + interface_language: sara_local_user.interface_language, + show_avatars: sara_local_user.show_avatars, + send_notifications_to_email: sara_local_user.send_notifications_to_email, + show_bot_accounts: sara_local_user.show_bot_accounts, + show_read_posts: sara_local_user.show_read_posts, + email_verified: sara_local_user.email_verified, + accepted_application: sara_local_user.accepted_application, + totp_2fa_secret: sara_local_user.totp_2fa_secret, + password_encrypted: sara_local_user.password_encrypted, + open_links_in_new_tab: sara_local_user.open_links_in_new_tab, + infinite_scroll_enabled: sara_local_user.infinite_scroll_enabled, + post_listing_mode: sara_local_user.post_listing_mode, + totp_2fa_enabled: sara_local_user.totp_2fa_enabled, + enable_keyboard_navigation: sara_local_user.enable_keyboard_navigation, + enable_animated_images: sara_local_user.enable_animated_images, + enable_private_messages: sara_local_user.enable_private_messages, + collapse_bot_comments: sara_local_user.collapse_bot_comments, + last_donation_notification: sara_local_user.last_donation_notification, + show_upvotes: sara_local_user.show_upvotes, + show_downvotes: sara_local_user.show_downvotes, ..Default::default() }, creator: Person { - id: inserted_sara_person.id, - name: inserted_sara_person.name.clone(), + id: sara_person.id, + name: sara_person.name.clone(), display_name: None, - published: inserted_sara_person.published, + published: sara_person.published, avatar: None, - actor_id: inserted_sara_person.actor_id.clone(), + ap_id: sara_person.ap_id.clone(), local: true, banned: false, ban_expires: None, @@ -259,12 +236,16 @@ mod tests { bio: None, banner: None, updated: None, - inbox_url: inserted_sara_person.inbox_url.clone(), + inbox_url: sara_person.inbox_url.clone(), matrix_user_id: None, - instance_id: inserted_instance.id, - private_key: inserted_sara_person.private_key, - public_key: inserted_sara_person.public_key, - last_refreshed_at: inserted_sara_person.last_refreshed_at, + instance_id: instance.id, + private_key: sara_person.private_key, + public_key: sara_person.public_key, + last_refreshed_at: sara_person.last_refreshed_at, + post_count: 0, + post_score: 0, + comment_count: 0, + comment_score: 0, }, admin: None, }; @@ -290,7 +271,7 @@ mod tests { // Approve the application let approve_form = RegistrationApplicationUpdateForm { - admin_id: Some(Some(inserted_timmy_person.id)), + admin_id: Some(Some(timmy_person.id)), deny_reason: None, }; @@ -302,7 +283,7 @@ mod tests { ..Default::default() }; - LocalUser::update(pool, inserted_sara_local_user.id, &approve_local_user_form).await?; + LocalUser::update(pool, sara_local_user.id, &approve_local_user_form).await?; let read_sara_app_view_after_approve = RegistrationApplicationView::read(pool, sara_app.id).await?; @@ -311,15 +292,15 @@ mod tests { expected_sara_app_view .creator_local_user .accepted_application = true; - expected_sara_app_view.registration_application.admin_id = Some(inserted_timmy_person.id); + expected_sara_app_view.registration_application.admin_id = Some(timmy_person.id); expected_sara_app_view.admin = Some(Person { - id: inserted_timmy_person.id, - name: inserted_timmy_person.name.clone(), + id: timmy_person.id, + name: timmy_person.name.clone(), display_name: None, - published: inserted_timmy_person.published, + published: timmy_person.published, avatar: None, - actor_id: inserted_timmy_person.actor_id.clone(), + ap_id: timmy_person.ap_id.clone(), local: true, banned: false, ban_expires: None, @@ -328,12 +309,16 @@ mod tests { bio: None, banner: None, updated: None, - inbox_url: inserted_timmy_person.inbox_url.clone(), + inbox_url: timmy_person.inbox_url.clone(), matrix_user_id: None, - instance_id: inserted_instance.id, - private_key: inserted_timmy_person.private_key, - public_key: inserted_timmy_person.public_key, - last_refreshed_at: inserted_timmy_person.last_refreshed_at, + instance_id: instance.id, + private_key: timmy_person.private_key, + public_key: timmy_person.public_key, + last_refreshed_at: timmy_person.last_refreshed_at, + post_count: 0, + post_score: 0, + comment_count: 0, + comment_score: 0, }); assert_eq!(read_sara_app_view_after_approve, expected_sara_app_view); @@ -356,10 +341,10 @@ mod tests { let all_apps = RegistrationApplicationQuery::default().list(pool).await?; assert_eq!(all_apps.len(), 2); - Person::delete(pool, inserted_timmy_person.id).await?; - Person::delete(pool, inserted_sara_person.id).await?; + Person::delete(pool, timmy_person.id).await?; + Person::delete(pool, sara_person.id).await?; Person::delete(pool, inserted_jess_person.id).await?; - Instance::delete(pool, inserted_instance.id).await?; + Instance::delete(pool, instance.id).await?; Ok(()) } diff --git a/crates/db_views/src/reports/comment_report_view.rs b/crates/db_views/src/reports/comment_report_view.rs new file mode 100644 index 000000000..01f8211e6 --- /dev/null +++ b/crates/db_views/src/reports/comment_report_view.rs @@ -0,0 +1,140 @@ +use crate::structs::CommentReportView; +use diesel::{ + dsl::now, + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + impls::community::community_follower_select_subscribed_type, + newtypes::{CommentReportId, PersonId}, + schema::{ + comment, + comment_actions, + comment_report, + community, + community_actions, + local_user, + person, + person_actions, + post, + }, + utils::{functions::coalesce, get_conn, DbPool}, +}; + +impl CommentReportView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(my_person_id: PersonId) -> _ { + let recipient_id = aliases::person1.field(person::id); + let resolver_id = aliases::person2.field(person::id); + + let post_join = post::table.on(comment::post_id.eq(post::id)); + + let community_join = community::table.on(post::community_id.eq(community::id)); + + let report_creator_join = person::table.on(comment_report::creator_id.eq(person::id)); + + let local_user_join = local_user::table.on( + comment::creator_id + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ); + + let comment_creator_join = aliases::person1.on(comment::creator_id.eq(recipient_id)); + + let comment_actions_join = comment_actions::table.on( + comment_actions::comment_id + .eq(comment_report::comment_id) + .and(comment_actions::person_id.eq(my_person_id)), + ); + + let resolver_join = aliases::person2.on(comment_report::resolver_id.eq(resolver_id.nullable())); + + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(comment::creator_id), + ), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(comment::creator_id) + .and(person_actions::person_id.eq(my_person_id)), + ); + + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(post::community_id) + .and(community_actions::person_id.eq(my_person_id)), + ); + + comment_report::table + .inner_join(comment::table) + .inner_join(post_join) + .inner_join(community_join) + .inner_join(report_creator_join) + .inner_join(comment_creator_join) + .left_join(comment_actions_join) + .left_join(resolver_join) + .left_join(creator_community_actions_join) + .left_join(local_user_join) + .left_join(person_actions_join) + .left_join(community_actions_join) + } + + /// returns the CommentReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: CommentReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + Self::joins(my_person_id) + .filter(comment_report::id.eq(report_id)) + .select(( + comment_report::all_columns, + comment::all_columns, + post::all_columns, + community::all_columns, + person::all_columns, + aliases::person1.fields(person::all_columns), + 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_actions::blocked.nullable().is_not_null(), + community_follower_select_subscribed_type(), + comment_actions::saved.nullable(), + comment_actions::like_score.nullable(), + aliases::person2.fields(person::all_columns).nullable(), + )) + .first(conn) + .await + } +} diff --git a/crates/db_views/src/reports/community_report_view.rs b/crates/db_views/src/reports/community_report_view.rs new file mode 100644 index 000000000..e5a938cad --- /dev/null +++ b/crates/db_views/src/reports/community_report_view.rs @@ -0,0 +1,55 @@ +use crate::structs::CommunityReportView; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases, + impls::community::community_follower_select_subscribed_type, + newtypes::{CommunityReportId, PersonId}, + schema::{community, community_actions, community_report, person}, + utils::{get_conn, DbPool}, +}; + +impl CommunityReportView { + /// returns the CommunityReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: CommunityReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(community_report::community_id) + .and(community_actions::person_id.eq(my_person_id)), + ); + + community_report::table + .find(report_id) + .inner_join(community::table) + .inner_join(person::table.on(community_report::creator_id.eq(person::id))) + .left_join( + aliases::person2 + .on(community_report::resolver_id.eq(aliases::person2.field(person::id).nullable())), + ) + .left_join(community_actions_join) + .select(( + community_report::all_columns, + community::all_columns, + person::all_columns, + community_follower_select_subscribed_type(), + aliases::person2.fields(person::all_columns.nullable()), + )) + .first(conn) + .await + } +} diff --git a/crates/db_views/src/reports/mod.rs b/crates/db_views/src/reports/mod.rs new file mode 100644 index 000000000..a74b9470d --- /dev/null +++ b/crates/db_views/src/reports/mod.rs @@ -0,0 +1,8 @@ +#[cfg(feature = "full")] +pub mod comment_report_view; +#[cfg(feature = "full")] +pub mod community_report_view; +#[cfg(feature = "full")] +pub mod post_report_view; +#[cfg(feature = "full")] +pub mod private_message_report_view; diff --git a/crates/db_views/src/reports/post_report_view.rs b/crates/db_views/src/reports/post_report_view.rs new file mode 100644 index 000000000..c9fae7db3 --- /dev/null +++ b/crates/db_views/src/reports/post_report_view.rs @@ -0,0 +1,132 @@ +use crate::structs::PostReportView; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + impls::community::community_follower_select_subscribed_type, + newtypes::{PersonId, PostReportId}, + schema::{ + community, + community_actions, + local_user, + person, + person_actions, + post, + post_actions, + post_report, + }, + utils::{functions::coalesce, get_conn, DbPool}, +}; + +impl PostReportView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins(my_person_id: PersonId) -> _ { + let recipient_id = aliases::person1.field(person::id); + let resolver_id = aliases::person2.field(person::id); + + let community_join = community::table.on(post::community_id.eq(community::id)); + + let report_creator_join = person::table.on(post_report::creator_id.eq(person::id)); + + let post_creator_join = aliases::person1.on(post::creator_id.eq(recipient_id)); + + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(post::creator_id), + ), + ); + + let community_actions_join = community_actions::table.on( + community_actions::community_id + .eq(post::community_id) + .and(community_actions::person_id.eq(my_person_id)), + ); + + let local_user_join = local_user::table.on( + post::creator_id + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ); + + let post_actions_join = post_actions::table.on( + post_actions::post_id + .eq(post::id) + .and(post_actions::person_id.eq(my_person_id)), + ); + + let person_actions_join = person_actions::table.on( + person_actions::target_id + .eq(post::creator_id) + .and(person_actions::person_id.eq(my_person_id)), + ); + + let resolver_join = aliases::person2.on(post_report::resolver_id.eq(resolver_id.nullable())); + + post_report::table + .inner_join(post::table) + .inner_join(community_join) + .inner_join(report_creator_join) + .inner_join(post_creator_join) + .left_join(creator_community_actions_join) + .left_join(community_actions_join) + .left_join(local_user_join) + .left_join(post_actions_join) + .left_join(person_actions_join) + .left_join(resolver_join) + } + + /// returns the PostReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: PostReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + Self::joins(my_person_id) + .filter(post_report::id.eq(report_id)) + .select(( + post_report::all_columns, + post::all_columns, + community::all_columns, + person::all_columns, + aliases::person1.fields(person::all_columns), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + local_user::admin.nullable().is_not_null(), + community_follower_select_subscribed_type(), + post_actions::saved.nullable(), + 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::comments.nullable() - post_actions::read_comments_amount.nullable(), + post::comments, + ), + aliases::person2.fields(person::all_columns.nullable()), + )) + .first(conn) + .await + } +} diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/reports/private_message_report_view.rs similarity index 100% rename from crates/db_views/src/private_message_report_view.rs rename to crates/db_views/src/reports/private_message_report_view.rs diff --git a/crates/db_views/src/custom_emoji_view.rs b/crates/db_views/src/site/custom_emoji_view.rs similarity index 71% rename from crates/db_views/src/custom_emoji_view.rs rename to crates/db_views/src/site/custom_emoji_view.rs index 606e807e9..a47e06275 100644 --- a/crates/db_views/src/custom_emoji_view.rs +++ b/crates/db_views/src/site/custom_emoji_view.rs @@ -1,5 +1,12 @@ use crate::structs::CustomEmojiView; -use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl}; +use diesel::{ + dsl::Nullable, + result::Error, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::CustomEmojiId, @@ -9,20 +16,33 @@ use lemmy_db_schema::{ }; use std::collections::HashMap; +type SelectionType = ( + ::AllColumns, + Nullable<::AllColumns>, +); + +fn selection() -> SelectionType { + ( + custom_emoji::all_columns, + custom_emoji_keyword::all_columns.nullable(), // (or all the columns if you want) + ) +} type CustomEmojiTuple = (CustomEmoji, Option); +// TODO this type is a mess, it should not be using vectors in a view. impl CustomEmojiView { + #[diesel::dsl::auto_type(no_type_alias)] + fn joins() -> _ { + custom_emoji::table.left_join( + custom_emoji_keyword::table.on(custom_emoji_keyword::custom_emoji_id.eq(custom_emoji::id)), + ) + } + pub async fn get(pool: &mut DbPool<'_>, emoji_id: CustomEmojiId) -> Result { let conn = &mut get_conn(pool).await?; - let emojis = custom_emoji::table - .find(emoji_id) - .left_join( - custom_emoji_keyword::table.on(custom_emoji_keyword::custom_emoji_id.eq(custom_emoji::id)), - ) - .select(( - custom_emoji::all_columns, - custom_emoji_keyword::all_columns.nullable(), // (or all the columns if you want) - )) + let emojis = Self::joins() + .filter(custom_emoji::id.eq(emoji_id)) + .select(selection()) .load::(conn) .await?; if let Some(emoji) = CustomEmojiView::from_tuple_to_vec(emojis) @@ -44,12 +64,7 @@ impl CustomEmojiView { ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let mut query = custom_emoji::table - .left_join( - custom_emoji_keyword::table.on(custom_emoji_keyword::custom_emoji_id.eq(custom_emoji::id)), - ) - .order(custom_emoji::category) - .into_boxed(); + let mut query = Self::joins().into_boxed(); if !ignore_page_limits { let (limit, offset) = limit_and_offset(page, limit)?; @@ -60,13 +75,10 @@ impl CustomEmojiView { query = query.filter(custom_emoji::category.eq(category)) } - query = query.then_order_by(custom_emoji::id); - let emojis = query - .select(( - custom_emoji::all_columns, - custom_emoji_keyword::all_columns.nullable(), // (or all the columns if you want) - )) + .select(selection()) + .order(custom_emoji::category) + .then_order_by(custom_emoji::id) .load::(conn) .await?; diff --git a/crates/db_views/src/local_image_view.rs b/crates/db_views/src/site/local_image_view.rs similarity index 50% rename from crates/db_views/src/local_image_view.rs rename to crates/db_views/src/site/local_image_view.rs index 7b5b97095..3d342528c 100644 --- a/crates/db_views/src/local_image_view.rs +++ b/crates/db_views/src/site/local_image_view.rs @@ -1,5 +1,5 @@ use crate::structs::LocalImageView; -use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl, SelectableHelper}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::LocalUserId, @@ -8,31 +8,11 @@ use lemmy_db_schema::{ }; impl LocalImageView { - async fn get_all_helper( - pool: &mut DbPool<'_>, - user_id: Option, - page: Option, - limit: Option, - ignore_page_limits: bool, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let mut query = local_image::table + #[diesel::dsl::auto_type(no_type_alias)] + fn joins() -> _ { + local_image::table .inner_join(local_user::table) .inner_join(person::table.on(local_user::person_id.eq(person::id))) - .select((local_image::all_columns, person::all_columns)) - .order_by(local_image::published.desc()) - .into_boxed(); - - if let Some(user_id) = user_id { - query = query.filter(local_image::local_user_id.eq(user_id)) - } - - if !ignore_page_limits { - let (limit, offset) = limit_and_offset(page, limit)?; - query = query.limit(limit).offset(offset); - } - - query.load::(conn).await } pub async fn get_all_paged_by_local_user_id( @@ -41,14 +21,28 @@ impl LocalImageView { page: Option, limit: Option, ) -> Result, Error> { - Self::get_all_helper(pool, Some(user_id), page, limit, false).await + let conn = &mut get_conn(pool).await?; + let (limit, offset) = limit_and_offset(page, limit)?; + + Self::joins() + .filter(local_image::local_user_id.eq(user_id)) + .select(Self::as_select()) + .limit(limit) + .offset(offset) + .load::(conn) + .await } pub async fn get_all_by_local_user_id( pool: &mut DbPool<'_>, user_id: LocalUserId, ) -> Result, Error> { - Self::get_all_helper(pool, Some(user_id), None, None, true).await + let conn = &mut get_conn(pool).await?; + Self::joins() + .filter(local_image::local_user_id.eq(user_id)) + .select(Self::as_select()) + .load::(conn) + .await } pub async fn get_all( @@ -56,6 +50,13 @@ impl LocalImageView { page: Option, limit: Option, ) -> Result, Error> { - Self::get_all_helper(pool, None, page, limit, false).await + let conn = &mut get_conn(pool).await?; + let (limit, offset) = limit_and_offset(page, limit)?; + Self::joins() + .select(Self::as_select()) + .limit(limit) + .offset(offset) + .load::(conn) + .await } } diff --git a/crates/db_views/src/site/mod.rs b/crates/db_views/src/site/mod.rs new file mode 100644 index 000000000..f2c318409 --- /dev/null +++ b/crates/db_views/src/site/mod.rs @@ -0,0 +1,8 @@ +#[cfg(feature = "full")] +pub mod custom_emoji_view; +#[cfg(feature = "full")] +pub mod local_image_view; +#[cfg(feature = "full")] +pub mod site_view; +#[cfg(feature = "full")] +pub mod vote_view; diff --git a/crates/db_views/src/site_view.rs b/crates/db_views/src/site/site_view.rs similarity index 69% rename from crates/db_views/src/site_view.rs rename to crates/db_views/src/site/site_view.rs index ed9aeb498..86e1dc962 100644 --- a/crates/db_views/src/site_view.rs +++ b/crates/db_views/src/site/site_view.rs @@ -1,8 +1,8 @@ use crate::structs::SiteView; -use diesel::{ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl}; +use diesel::{ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl, SelectableHelper}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - schema::{local_site, local_site_rate_limit, site, site_aggregates}, + schema::{local_site, local_site_rate_limit, site}, utils::{get_conn, DbPool}, }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -16,13 +16,7 @@ impl SiteView { .inner_join( local_site_rate_limit::table.on(local_site::id.eq(local_site_rate_limit::local_site_id)), ) - .inner_join(site_aggregates::table) - .select(( - site::all_columns, - local_site::all_columns, - local_site_rate_limit::all_columns, - site_aggregates::all_columns, - )) + .select(Self::as_select()) .first(conn) .await .optional()? diff --git a/crates/db_views/src/vote_view.rs b/crates/db_views/src/site/vote_view.rs similarity index 81% rename from crates/db_views/src/vote_view.rs rename to crates/db_views/src/site/vote_view.rs index 827cd3cc9..b6dd61fb5 100644 --- a/crates/db_views/src/vote_view.rs +++ b/crates/db_views/src/site/vote_view.rs @@ -1,11 +1,18 @@ use crate::structs::VoteView; -use diesel::{result::Error, ExpressionMethods, NullableExpressionMethods, QueryDsl}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::creator_community_actions, newtypes::{CommentId, PostId}, schema::{comment, comment_actions, community_actions, person, post, post_actions}, - utils::{action_query, actions_alias, get_conn, limit_and_offset, DbPool}, + utils::{get_conn, limit_and_offset, DbPool}, }; impl VoteView { @@ -18,14 +25,22 @@ impl VoteView { let conn = &mut get_conn(pool).await?; let (limit, offset) = limit_and_offset(page, limit)?; - action_query(post_actions::like_score) + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(post_actions::person_id), + ), + ); + + post_actions::table + .filter(post_actions::like_score.is_not_null()) .inner_join(person::table) .inner_join(post::table) - .left_join(actions_alias( - creator_community_actions, - post_actions::person_id, - post::community_id, - )) + .left_join(creator_community_actions_join) .filter(post_actions::post_id.eq(post_id)) .select(( person::all_columns, @@ -51,14 +66,22 @@ impl VoteView { let conn = &mut get_conn(pool).await?; let (limit, offset) = limit_and_offset(page, limit)?; - action_query(comment_actions::like_score) + let creator_community_actions_join = creator_community_actions.on( + creator_community_actions + .field(community_actions::community_id) + .eq(post::community_id) + .and( + creator_community_actions + .field(community_actions::person_id) + .eq(comment_actions::person_id), + ), + ); + + comment_actions::table + .filter(comment_actions::like_score.is_not_null()) .inner_join(person::table) .inner_join(comment::table.inner_join(post::table)) - .left_join(actions_alias( - creator_community_actions, - comment_actions::person_id, - post::community_id, - )) + .left_join(creator_community_actions_join) .filter(comment_actions::comment_id.eq(comment_id)) .select(( person::all_columns, @@ -141,7 +164,7 @@ mod tests { 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 = [ + let mut expected_post_vote_views = [ VoteView { creator: inserted_sara.clone(), creator_banned_from_community: false, @@ -153,6 +176,8 @@ mod tests { score: 1, }, ]; + expected_post_vote_views[1].creator.post_count = 1; + expected_post_vote_views[1].creator.comment_count = 1; let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None).await?; assert_eq!(read_post_vote_views, expected_post_vote_views); @@ -173,7 +198,7 @@ mod tests { }; CommentLike::like(pool, &sara_comment_vote_form).await?; - let expected_comment_vote_views = [ + let mut expected_comment_vote_views = [ VoteView { creator: inserted_timmy.clone(), creator_banned_from_community: false, @@ -185,6 +210,8 @@ mod tests { score: 1, }, ]; + expected_comment_vote_views[0].creator.post_count = 1; + expected_comment_vote_views[0].creator.comment_count = 1; let read_comment_vote_views = VoteView::list_for_comment(pool, inserted_comment.id, None, None).await?; diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index eedaa7980..3d7478ab9 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -1,28 +1,70 @@ +use chrono::{DateTime, Utc}; #[cfg(feature = "full")] -use diesel::Queryable; +use diesel::{ + deserialize::FromSqlRow, + dsl::exists, + + dsl::Nullable, + expression::AsExpression, + sql_types, + BoolExpressionMethods, + ExpressionMethods, + NullableExpressionMethods, + PgExpressionMethods, + QueryDsl, + Queryable, + Selectable, +}; #[cfg(feature = "full")] -use diesel::{deserialize::FromSqlRow, expression::AsExpression, sql_types}; use lemmy_db_schema::{ - aggregates::structs::{ - CommentAggregates, - CommunityAggregates, - PersonAggregates, - PostAggregates, - SiteAggregates, - }, + aliases::{creator_community_actions, creator_local_user, person1}, + impls::comment::comment_select_remove_deletes, + impls::community::community_follower_select_subscribed_type, + impls::local_user::local_user_can_mod, + schema::{comment, comment_actions, community_actions, local_user, person, person_actions}, + utils::functions::coalesce, + Person1AliasAllColumnsTuple, +}; +use lemmy_db_schema::{ source::{ comment::Comment, + comment_reply::CommentReply, comment_report::CommentReport, community::Community, community_report::CommunityReport, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, images::{ImageDetails, LocalImage}, + instance::Instance, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, - local_user_vote_display_mode::LocalUserVoteDisplayMode, + mod_log::{ + admin::{ + AdminAllowInstance, + AdminBlockInstance, + AdminPurgeComment, + AdminPurgeCommunity, + AdminPurgePerson, + AdminPurgePost, + }, + moderator::{ + ModAdd, + ModAddCommunity, + ModBan, + ModBanFromCommunity, + ModFeaturePost, + ModHideCommunity, + ModLockPost, + ModRemoveComment, + ModRemoveCommunity, + ModRemovePost, + ModTransferCommunity, + }, + }, person::Person, + person_comment_mention::PersonCommentMention, + person_post_mention::PersonPostMention, post::Post, post_report::PostReport, private_message::PrivateMessage, @@ -51,13 +93,14 @@ pub struct CommentReportView { pub community: Community, pub creator: Person, pub comment_creator: Person, - pub counts: CommentAggregates, pub creator_banned_from_community: bool, pub creator_is_moderator: bool, pub creator_is_admin: bool, pub creator_blocked: bool, pub subscribed: SubscribedType, - pub saved: bool, + #[cfg_attr(feature = "full", ts(optional))] + /// The time when the comment was saved. + pub saved: Option>, #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, #[cfg_attr(feature = "full", ts(optional))] @@ -66,25 +109,120 @@ pub struct CommentReportView { #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A comment view. pub struct CommentView { + #[cfg_attr(feature = "full", + diesel( + select_expression = comment_select_remove_deletes() + ) + )] + pub comment: Comment, + #[cfg_attr(feature = "full", diesel(embed))] + pub creator: Person, + #[cfg_attr(feature = "full", diesel(embed))] + pub post: Post, + #[cfg_attr(feature = "full", diesel(embed))] + pub community: Community, + #[cfg_attr(feature = "full", + diesel( + select_expression = + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null() + ) + )] + pub creator_banned_from_community: bool, + #[cfg_attr(feature = "full", + diesel( + select_expression = + community_actions::received_ban.nullable().is_not_null() + ) + )] + pub banned_from_community: bool, + #[cfg_attr(feature = "full", + diesel( + select_expression = + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null() + ) + )] + pub creator_is_moderator: bool, + #[cfg_attr(feature = "full", + diesel( + select_expression = + exists(creator_local_user.filter( + comment::creator_id + .eq(creator_local_user.field(local_user::person_id)) + .and(creator_local_user.field(local_user::admin).eq(true)), + )) + ) + )] + pub creator_is_admin: bool, + #[cfg_attr(feature = "full", + diesel( + select_expression = community_follower_select_subscribed_type(), + ) + )] + pub subscribed: SubscribedType, + #[cfg_attr(feature = "full", ts(optional))] + #[cfg_attr(feature = "full", + diesel( + select_expression = + comment_actions::saved.nullable() + ) + )] + /// The time when the comment was saved. + pub saved: Option>, + #[cfg_attr(feature = "full", + diesel( + select_expression = + person_actions::blocked.nullable().is_not_null() + ) + )] + pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] + #[cfg_attr(feature = "full", + diesel( + select_expression = + comment_actions::like_score.nullable() + ) + )] + pub my_vote: Option, + #[cfg_attr(feature = "full", + diesel( + select_expression = local_user_can_mod() + ) + )] + pub can_mod: bool, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A slimmer comment view, without the post, or community. +pub struct CommentSlimView { pub comment: Comment, pub creator: Person, - pub post: Post, - pub community: Community, - pub counts: CommentAggregates, pub creator_banned_from_community: bool, pub banned_from_community: bool, pub creator_is_moderator: bool, pub creator_is_admin: bool, pub subscribed: SubscribedType, - pub saved: bool, + #[cfg_attr(feature = "full", ts(optional))] + /// The time when the comment was saved. + pub saved: Option>, pub creator_blocked: bool, #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, + pub can_mod: bool, } #[skip_serializing_none] @@ -97,22 +235,21 @@ pub struct CommunityReportView { pub community_report: CommunityReport, pub community: Community, pub creator: Person, - pub counts: CommunityAggregates, pub subscribed: SubscribedType, #[cfg_attr(feature = "full", ts(optional))] pub resolver: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A local user view. pub struct LocalUserView { + #[cfg_attr(feature = "full", diesel(embed))] pub local_user: LocalUser, - pub local_user_vote_display_mode: LocalUserVoteDisplayMode, + #[cfg_attr(feature = "full", diesel(embed))] pub person: Person, - pub counts: PersonAggregates, } #[skip_serializing_none] @@ -131,14 +268,15 @@ pub struct PostReportView { pub creator_is_moderator: bool, pub creator_is_admin: bool, pub subscribed: SubscribedType, - pub saved: bool, + #[cfg_attr(feature = "full", ts(optional))] + /// The time when the post was saved. + pub saved: Option>, pub read: bool, pub hidden: bool, pub creator_blocked: bool, #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, pub unread_comments: i64, - pub counts: PostAggregates, #[cfg_attr(feature = "full", ts(optional))] pub resolver: Option, } @@ -147,28 +285,11 @@ pub struct PostReportView { /// perspective. stringified since we might want to use arbitrary info later, with a P prepended to /// prevent ossification (api users love to make assumptions (e.g. parse stuff that looks like /// numbers as numbers) about apis that aren't part of the spec +// TODO this is a mess, get rid of it and prefer the one in db_schema #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -pub struct PaginationCursor(pub String); - -/// like PaginationCursor but for the report_combined table -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -pub struct ReportCombinedPaginationCursor(pub String); - -/// like PaginationCursor but for the person_content_combined table -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -pub struct PersonContentCombinedPaginationCursor(pub String); - -/// like PaginationCursor but for the person_saved_combined table -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -pub struct PersonSavedCombinedPaginationCursor(pub String); +pub struct PostPaginationCursor(pub String); #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -186,9 +307,10 @@ pub struct PostView { pub banned_from_community: bool, pub creator_is_moderator: bool, pub creator_is_admin: bool, - pub counts: PostAggregates, pub subscribed: SubscribedType, - pub saved: bool, + #[cfg_attr(feature = "full", ts(optional))] + /// The time when the post was saved. + pub saved: Option>, pub read: bool, pub hidden: bool, pub creator_blocked: bool, @@ -196,6 +318,7 @@ pub struct PostView { pub my_vote: Option, pub unread_comments: i64, pub tags: PostTags, + pub can_mod: bool, } #[skip_serializing_none] @@ -215,28 +338,39 @@ pub struct PrivateMessageReportView { #[skip_serializing_none] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A registration application view. pub struct RegistrationApplicationView { + #[cfg_attr(feature = "full", diesel(embed))] pub registration_application: RegistrationApplication, + #[cfg_attr(feature = "full", diesel(embed))] pub creator_local_user: LocalUser, + #[cfg_attr(feature = "full", diesel(embed))] pub creator: Person, #[cfg_attr(feature = "full", ts(optional))] + #[cfg_attr(feature = "full", + diesel( + select_expression_type = Nullable, + select_expression = person1.fields(person::all_columns).nullable() + ) + )] pub admin: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A site view. pub struct SiteView { + #[cfg_attr(feature = "full", diesel(embed))] pub site: Site, + #[cfg_attr(feature = "full", diesel(embed))] pub local_site: LocalSite, + #[cfg_attr(feature = "full", diesel(embed))] pub local_site_rate_limit: LocalSiteRateLimit, - pub counts: SiteAggregates, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -263,12 +397,14 @@ pub struct VoteView { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A local image view. pub struct LocalImageView { + #[cfg_attr(feature = "full", diesel(embed))] pub local_image: LocalImage, + #[cfg_attr(feature = "full", diesel(embed))] pub person: Person, } @@ -280,24 +416,21 @@ pub struct ReportCombinedViewInternal { // Post-specific pub post_report: Option, pub post: Option, - pub post_counts: Option, pub post_unread_comments: Option, - pub post_saved: bool, + pub post_saved: Option>, pub post_read: bool, pub post_hidden: bool, pub my_post_vote: Option, // Comment-specific pub comment_report: Option, pub comment: Option, - pub comment_counts: Option, - pub comment_saved: bool, + pub comment_saved: Option>, pub my_comment_vote: Option, // Private-message-specific pub private_message_report: Option, pub private_message: Option, // Community-specific pub community_report: Option, - pub community_counts: Option, // Shared pub report_creator: Person, pub item_creator: Option, @@ -326,11 +459,10 @@ pub enum ReportCombinedView { #[cfg_attr(feature = "full", derive(Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] /// A combined person_content view -pub struct PersonContentViewInternal { +pub(crate) struct PersonContentCombinedViewInternal { // Post-specific - pub post_counts: PostAggregates, pub post_unread_comments: i64, - pub post_saved: bool, + pub post_saved: Option>, pub post_read: bool, pub post_hidden: bool, pub my_post_vote: Option, @@ -338,8 +470,7 @@ pub struct PersonContentViewInternal { pub post_tags: PostTags, // Comment-specific pub comment: Option, - pub comment_counts: Option, - pub comment_saved: bool, + pub comment_saved: Option>, pub my_comment_vote: Option, // Shared pub post: Post, @@ -351,6 +482,7 @@ pub struct PersonContentViewInternal { pub item_creator_banned_from_community: bool, pub item_creator_blocked: bool, pub banned_from_community: bool, + pub can_mod: bool, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -363,6 +495,681 @@ pub enum PersonContentCombinedView { Comment(CommentView), } +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined person_saved view +pub(crate) struct PersonSavedCombinedViewInternal { + // Post-specific + pub post_unread_comments: i64, + pub post_saved: Option>, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + pub image_details: Option, + pub post_tags: PostTags, + // Comment-specific + pub comment: Option, + pub comment_saved: Option>, + pub my_comment_vote: Option, + // Shared + pub post: Post, + pub community: Community, + pub item_creator: Person, + pub subscribed: SubscribedType, + pub item_creator_is_admin: bool, + pub item_creator_is_moderator: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_blocked: bool, + pub banned_from_community: bool, + pub can_mod: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum PersonSavedCombinedView { + Post(PostView), + Comment(CommentView), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A community follower. +pub struct CommunityFollowerView { + #[cfg_attr(feature = "full", diesel(embed))] + pub community: Community, + #[cfg_attr(feature = "full", diesel(embed))] + pub follower: Person, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A community moderator. +pub struct CommunityModeratorView { + #[cfg_attr(feature = "full", diesel(embed))] + pub community: Community, + #[cfg_attr(feature = "full", diesel(embed))] + pub moderator: Person, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A community person ban. +pub struct CommunityPersonBanView { + #[cfg_attr(feature = "full", diesel(embed))] + pub community: Community, + #[cfg_attr(feature = "full", diesel(embed))] + pub person: Person, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A community view. +pub struct CommunityView { + #[cfg_attr(feature = "full", diesel(embed))] + pub community: Community, + #[cfg_attr(feature = "full", + diesel( + select_expression = community_follower_select_subscribed_type() + ) + )] + pub subscribed: SubscribedType, + #[cfg_attr(feature = "full", + diesel( + select_expression = community_actions::blocked.nullable().is_not_null() + ) + )] + pub blocked: bool, + #[cfg_attr(feature = "full", + diesel( + select_expression = community_actions::received_ban.nullable().is_not_null() + ) + )] + pub banned_from_community: bool, + #[cfg_attr(feature = "full", + diesel( + select_expression = local_user::admin.nullable() + .or(community_actions::became_moderator.nullable().is_not_null()) + .is_not_distinct_from(true) + ) + )] + pub can_mod: bool, +} + +/// The community sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub enum CommunitySortType { + ActiveSixMonths, + #[default] + ActiveMonthly, + ActiveWeekly, + ActiveDaily, + Hot, + New, + Old, + NameAsc, + NameDesc, + Comments, + Posts, + Subscribers, + SubscribersLocal, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A person comment mention view. +pub struct PersonCommentMentionView { + pub person_comment_mention: PersonCommentMention, + pub comment: Comment, + pub creator: Person, + pub post: Post, + pub community: Community, + pub recipient: Person, + pub creator_banned_from_community: bool, + pub banned_from_community: bool, + pub creator_is_moderator: bool, + pub creator_is_admin: bool, + pub subscribed: SubscribedType, + #[cfg_attr(feature = "full", ts(optional))] + /// The time when the comment was saved. + pub saved: Option>, + pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub my_vote: Option, + pub can_mod: bool, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A person post mention view. +pub struct PersonPostMentionView { + pub person_post_mention: PersonPostMention, + pub post: Post, + pub creator: Person, + pub community: Community, + #[cfg_attr(feature = "full", ts(optional))] + pub image_details: Option, + pub recipient: Person, + pub creator_banned_from_community: bool, + pub banned_from_community: bool, + pub creator_is_moderator: bool, + pub creator_is_admin: bool, + pub subscribed: SubscribedType, + #[cfg_attr(feature = "full", ts(optional))] + /// The time when the post was saved. + pub saved: Option>, + pub read: bool, + pub hidden: bool, + pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub my_vote: Option, + pub unread_comments: i64, + pub post_tags: PostTags, + pub can_mod: bool, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A comment reply view. +pub struct CommentReplyView { + pub comment_reply: CommentReply, + pub comment: Comment, + pub creator: Person, + pub post: Post, + pub community: Community, + pub recipient: Person, + pub creator_banned_from_community: bool, + pub banned_from_community: bool, + pub creator_is_moderator: bool, + pub creator_is_admin: bool, + pub subscribed: SubscribedType, + #[cfg_attr(feature = "full", ts(optional))] + /// The time when the comment was saved. + pub saved: Option>, + pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub my_vote: Option, + pub can_mod: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A person view. +pub struct PersonView { + #[cfg_attr(feature = "full", diesel(embed))] + pub person: Person, + #[cfg_attr(feature = "full", + diesel( + select_expression_type = coalesce, bool>, + select_expression = coalesce(local_user::admin.nullable(), false) + ) + )] + pub is_admin: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct PendingFollow { + pub person: Person, + pub community: Community, + pub is_new_instance: bool, + pub subscribed: SubscribedType, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A private message view. +pub struct PrivateMessageView { + #[cfg_attr(feature = "full", diesel(embed))] + pub private_message: PrivateMessage, + #[cfg_attr(feature = "full", diesel(embed))] + pub creator: Person, + #[cfg_attr(feature = "full", + diesel( + select_expression_type = Person1AliasAllColumnsTuple, + select_expression = person1.fields(person::all_columns) + ) + )] + pub recipient: Person, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined inbox view +pub struct InboxCombinedViewInternal { + // Comment reply + pub comment_reply: Option, + // Person comment mention + pub person_comment_mention: Option, + // Person post mention + pub person_post_mention: Option, + pub post_unread_comments: Option, + pub post_saved: Option>, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + pub image_details: Option, + pub post_tags: PostTags, + // Private message + pub private_message: Option, + // Shared + pub post: Option, + pub community: Option, + pub comment: Option, + pub comment_saved: Option>, + pub my_comment_vote: Option, + pub subscribed: SubscribedType, + pub item_creator: Person, + pub item_recipient: Person, + pub item_creator_is_admin: bool, + pub item_creator_is_moderator: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_blocked: bool, + pub banned_from_community: bool, + pub can_mod: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum InboxCombinedView { + CommentReply(CommentReplyView), + CommentMention(PersonCommentMentionView), + PostMention(PersonPostMentionView), + PrivateMessage(PrivateMessageView), +} +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 someone is added as a community moderator. +pub struct ModAddCommunityView { + pub mod_add_community: ModAddCommunity, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub community: Community, + pub other_person: Person, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 someone is added as a site moderator. +pub struct ModAddView { + pub mod_add: ModAdd, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub other_person: Person, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 someone is banned from a community. +pub struct ModBanFromCommunityView { + pub mod_ban_from_community: ModBanFromCommunity, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub community: Community, + pub other_person: Person, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 someone is banned from the site. +pub struct ModBanView { + pub mod_ban: ModBan, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub other_person: Person, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 a community is hidden from public view. +pub struct ModHideCommunityView { + pub mod_hide_community: ModHideCommunity, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, + pub community: Community, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 a moderator locks a post (prevents new comments being made). +pub struct ModLockPostView { + pub mod_lock_post: ModLockPost, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub other_person: Person, + pub post: Post, + pub community: Community, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 a moderator removes a comment. +pub struct ModRemoveCommentView { + pub mod_remove_comment: ModRemoveComment, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub other_person: Person, + pub comment: Comment, + pub post: Post, + pub community: Community, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 a moderator removes a community. +pub struct ModRemoveCommunityView { + pub mod_remove_community: ModRemoveCommunity, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub community: Community, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 a moderator removes a post. +pub struct ModRemovePostView { + pub mod_remove_post: ModRemovePost, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub other_person: Person, + pub post: Post, + pub community: Community, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 a moderator features a post on a community (pins it to the top). +pub struct ModFeaturePostView { + pub mod_feature_post: ModFeaturePost, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub other_person: Person, + pub post: Post, + pub community: Community, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 a moderator transfers a community to a new owner. +pub struct ModTransferCommunityView { + pub mod_transfer_community: ModTransferCommunity, + #[cfg_attr(feature = "full", ts(optional))] + pub moderator: Option, + pub community: Community, + pub other_person: Person, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 comment. +pub struct AdminPurgeCommentView { + pub admin_purge_comment: AdminPurgeComment, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, + pub post: Post, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 community. +pub struct AdminPurgeCommunityView { + pub admin_purge_community: AdminPurgeCommunity, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 person. +pub struct AdminPurgePersonView { + pub admin_purge_person: AdminPurgePerson, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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 AdminPurgePostView { + pub admin_purge_post: AdminPurgePost, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, + pub community: Community, +} + +#[skip_serializing_none] +#[derive(Debug, PartialEq, 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, PartialEq, 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_allow_instance: AdminAllowInstance, + pub instance: Instance, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined modlog view +pub(crate) struct ModlogCombinedViewInternal { + // Specific + #[cfg_attr(feature = "full", diesel(embed))] + pub admin_allow_instance: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub admin_block_instance: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub admin_purge_comment: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub admin_purge_community: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub admin_purge_person: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub admin_purge_post: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_add: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_add_community: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_ban: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_ban_from_community: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_feature_post: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_hide_community: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_lock_post: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_remove_comment: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_remove_community: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_remove_post: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub mod_transfer_community: Option, + // Specific fields + + // Shared + #[cfg_attr(feature = "full", diesel(embed))] + pub moderator: Option, + #[cfg_attr(feature = "full", + diesel( + select_expression_type = Nullable, + select_expression = person1.fields(person::all_columns).nullable() + ) + )] + pub other_person: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub instance: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub community: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub post: Option, + #[cfg_attr(feature = "full", diesel(embed))] + pub comment: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum ModlogCombinedView { + AdminAllowInstance(AdminAllowInstanceView), + AdminBlockInstance(AdminBlockInstanceView), + AdminPurgeComment(AdminPurgeCommentView), + AdminPurgeCommunity(AdminPurgeCommunityView), + AdminPurgePerson(AdminPurgePersonView), + AdminPurgePost(AdminPurgePostView), + ModAdd(ModAddView), + ModAddCommunity(ModAddCommunityView), + ModBan(ModBanView), + ModBanFromCommunity(ModBanFromCommunityView), + ModFeaturePost(ModFeaturePostView), + ModHideCommunity(ModHideCommunityView), + ModLockPost(ModLockPostView), + ModRemoveComment(ModRemoveCommentView), + ModRemoveCommunity(ModRemoveCommunityView), + ModRemovePost(ModRemovePostView), + ModTransferCommunity(ModTransferCommunityView), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined search view +pub(crate) struct SearchCombinedViewInternal { + // Post-specific + pub post: Option, + pub post_unread_comments: Option, + pub post_saved: Option>, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + pub image_details: Option, + pub post_tags: PostTags, + // // Comment-specific + pub comment: Option, + pub comment_saved: Option>, + pub my_comment_vote: Option, + // // Community-specific + pub community: Option, + pub community_blocked: bool, + pub subscribed: SubscribedType, + // Shared + pub item_creator: Option, + pub item_creator_is_admin: bool, + pub item_creator_is_moderator: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_blocked: bool, + pub banned_from_community: bool, + pub can_mod: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum SearchCombinedView { + Post(PostView), + Comment(CommentView), + Community(CommunityView), + Person(PersonView), +} + #[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)] #[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))] #[serde(transparent)] diff --git a/crates/db_views/src/utils.rs b/crates/db_views/src/utils.rs new file mode 100644 index 000000000..71f4e2d36 --- /dev/null +++ b/crates/db_views/src/utils.rs @@ -0,0 +1,13 @@ +use diesel::{BoolExpressionMethods, ExpressionMethods}; +use lemmy_db_schema::schema::{community_actions, instance_actions, person_actions}; + +/// Hide all content from blocked communities and persons. Content from blocked instances is also +/// hidden, unless the user followed the community explicitly. +#[diesel::dsl::auto_type] +pub(crate) fn filter_blocked() -> _ { + instance_actions::blocked + .is_null() + .or(community_actions::followed.is_not_null()) + .and(community_actions::blocked.is_null()) + .and(person_actions::blocked.is_null()) +} diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml deleted file mode 100644 index 34d64d0b0..000000000 --- a/crates/db_views_actor/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -name = "lemmy_db_views_actor" -version.workspace = true -edition.workspace = true -description.workspace = true -license.workspace = true -homepage.workspace = true -documentation.workspace = true -repository.workspace = true - -[lib] -doctest = false - -[lints] -workspace = true - -[features] -full = [ - "lemmy_db_schema/full", - "lemmy_utils/full", - "i-love-jesus", - "diesel", - "diesel-async", - "ts-rs", -] - -[dependencies] -lemmy_db_schema = { workspace = true } -diesel = { workspace = true, features = [ - "chrono", - "postgres", - "serde_json", -], optional = true } -diesel-async = { workspace = true, features = [ - "deadpool", - "postgres", -], optional = true } -serde = { workspace = true } -serde_with = { workspace = true } -ts-rs = { workspace = true, optional = true } -chrono.workspace = true -strum = { workspace = true } -lemmy_utils = { workspace = true, optional = true } -i-love-jesus = { workspace = true, optional = true } - -[dev-dependencies] -serial_test = { workspace = true } -tokio = { workspace = true } -pretty_assertions = { workspace = true } -url.workspace = true diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs deleted file mode 100644 index bc12e6559..000000000 --- a/crates/db_views_actor/src/person_view.rs +++ /dev/null @@ -1,351 +0,0 @@ -use crate::structs::PersonView; -use diesel::{ - pg::Pg, - result::Error, - BoolExpressionMethods, - ExpressionMethods, - NullableExpressionMethods, - PgTextExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{local_user, person, person_aggregates}, - utils::{ - functions::coalesce, - fuzzy_search, - limit_and_offset, - now, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, - ListingType, - PostSortType, -}; -use serde::{Deserialize, Serialize}; -use strum::{Display, EnumString}; - -enum ListMode { - Admins, - Banned, - Query(PersonQuery), -} - -#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)] -/// The person sort types. Converted automatically from `SortType` -enum PersonSortType { - New, - Old, - MostComments, - CommentScore, - PostScore, - PostCount, -} - -fn post_to_person_sort_type(sort: PostSortType) -> PersonSortType { - use PostSortType::*; - match sort { - Active | Hot | Controversial => PersonSortType::CommentScore, - New | NewComments => PersonSortType::New, - MostComments => PersonSortType::MostComments, - Old => PersonSortType::Old, - _ => PersonSortType::CommentScore, - } -} - -fn queries<'a>( -) -> Queries, impl ListFn<'a, PersonView, ListMode>> { - let all_joins = move |query: person::BoxedQuery<'a, Pg>| { - query - .inner_join(person_aggregates::table) - .left_join(local_user::table) - .select(( - person::all_columns, - person_aggregates::all_columns, - coalesce(local_user::admin.nullable(), false), - )) - }; - - let read = move |mut conn: DbConn<'a>, params: (PersonId, bool)| async move { - let (person_id, is_admin) = params; - let mut query = all_joins(person::table.find(person_id).into_boxed()); - if !is_admin { - query = query.filter(person::deleted.eq(false)); - } - query.first(&mut conn).await - }; - - let list = move |mut conn: DbConn<'a>, mode: ListMode| async move { - let mut query = all_joins(person::table.into_boxed()).filter(person::deleted.eq(false)); - match mode { - ListMode::Admins => { - query = query - .filter(local_user::admin.eq(true)) - .filter(person::deleted.eq(false)) - .order_by(person::published); - } - ListMode::Banned => { - query = query - .filter( - person::banned.eq(true).and( - person::ban_expires - .is_null() - .or(person::ban_expires.gt(now().nullable())), - ), - ) - .filter(person::deleted.eq(false)); - } - ListMode::Query(o) => { - if let Some(search_term) = o.search_term { - let searcher = fuzzy_search(&search_term); - query = query - .filter(person::name.ilike(searcher.clone())) - .or_filter(person::display_name.ilike(searcher)); - } - - let sort = o.sort.map(post_to_person_sort_type); - query = match sort.unwrap_or(PersonSortType::CommentScore) { - PersonSortType::New => query.order_by(person::published.desc()), - PersonSortType::Old => query.order_by(person::published.asc()), - PersonSortType::MostComments => query.order_by(person_aggregates::comment_count.desc()), - PersonSortType::CommentScore => query.order_by(person_aggregates::comment_score.desc()), - PersonSortType::PostScore => query.order_by(person_aggregates::post_score.desc()), - PersonSortType::PostCount => query.order_by(person_aggregates::post_count.desc()), - }; - - let (limit, offset) = limit_and_offset(o.page, o.limit)?; - query = query.limit(limit).offset(offset); - - if let Some(listing_type) = o.listing_type { - query = match listing_type { - // return nothing as its not possible to follow users - ListingType::Subscribed => query.limit(0), - ListingType::Local => query.filter(person::local.eq(true)), - _ => query, - }; - } - } - } - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl PersonView { - pub async fn read( - pool: &mut DbPool<'_>, - person_id: PersonId, - is_admin: bool, - ) -> Result { - queries().read(pool, (person_id, is_admin)).await - } - - pub async fn admins(pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, ListMode::Admins).await - } - - pub async fn banned(pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, ListMode::Banned).await - } -} - -#[derive(Default)] -pub struct PersonQuery { - pub sort: Option, - pub search_term: Option, - pub listing_type: Option, - pub page: Option, - pub limit: Option, -} - -impl PersonQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, ListMode::Query(self)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use super::*; - use lemmy_db_schema::{ - assert_length, - source::{ - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, - person::{Person, PersonInsertForm, PersonUpdateForm}, - }, - traits::Crud, - utils::build_db_pool_for_tests, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - struct Data { - alice: Person, - alice_local_user: LocalUser, - bob: Person, - bob_local_user: LocalUser, - } - - async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let alice_form = PersonInsertForm { - local: Some(true), - ..PersonInsertForm::test_form(inserted_instance.id, "alice") - }; - let alice = Person::create(pool, &alice_form).await?; - let alice_local_user_form = LocalUserInsertForm::test_form(alice.id); - let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?; - - let bob_form = PersonInsertForm { - bot_account: Some(true), - local: Some(false), - ..PersonInsertForm::test_form(inserted_instance.id, "bob") - }; - let bob = Person::create(pool, &bob_form).await?; - let bob_local_user_form = LocalUserInsertForm::test_form(bob.id); - let bob_local_user = LocalUser::create(pool, &bob_local_user_form, vec![]).await?; - - Ok(Data { - alice, - alice_local_user, - bob, - bob_local_user, - }) - } - - async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { - LocalUser::delete(pool, data.alice_local_user.id).await?; - LocalUser::delete(pool, data.bob_local_user.id).await?; - Person::delete(pool, data.alice.id).await?; - Person::delete(pool, data.bob.id).await?; - Instance::delete(pool, data.bob.instance_id).await?; - Ok(()) - } - - #[tokio::test] - #[serial] - async fn exclude_deleted() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - Person::update( - pool, - data.alice.id, - &PersonUpdateForm { - deleted: Some(true), - ..Default::default() - }, - ) - .await?; - - let read = PersonView::read(pool, data.alice.id, false).await; - assert!(read.is_err()); - - // only admin can view deleted users - let read = PersonView::read(pool, data.alice.id, true).await; - assert!(read.is_ok()); - - let list = PersonQuery { - sort: Some(PostSortType::New), - ..Default::default() - } - .list(pool) - .await?; - assert_length!(1, list); - assert_eq!(list[0].person.id, data.bob.id); - - cleanup(data, pool).await - } - - #[tokio::test] - #[serial] - async fn list_banned() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - Person::update( - pool, - data.alice.id, - &PersonUpdateForm { - banned: Some(true), - ..Default::default() - }, - ) - .await?; - - let list = PersonView::banned(pool).await?; - assert_length!(1, list); - assert_eq!(list[0].person.id, data.alice.id); - - cleanup(data, pool).await - } - - #[tokio::test] - #[serial] - async fn list_admins() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - LocalUser::update( - pool, - data.alice_local_user.id, - &LocalUserUpdateForm { - admin: Some(true), - ..Default::default() - }, - ) - .await?; - - let list = PersonView::admins(pool).await?; - assert_length!(1, list); - assert_eq!(list[0].person.id, data.alice.id); - - let is_admin = PersonView::read(pool, data.alice.id, false).await?.is_admin; - assert!(is_admin); - - let is_admin = PersonView::read(pool, data.bob.id, false).await?.is_admin; - assert!(!is_admin); - - cleanup(data, pool).await - } - - #[tokio::test] - #[serial] - async fn listing_type() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - let list = PersonQuery { - listing_type: Some(ListingType::Local), - ..Default::default() - } - .list(pool) - .await?; - assert_length!(1, list); - assert_eq!(list[0].person.id, data.alice.id); - - let list = PersonQuery { - listing_type: Some(ListingType::All), - ..Default::default() - } - .list(pool) - .await?; - assert_length!(2, list); - - cleanup(data, pool).await - } -} diff --git a/crates/db_views_actor/src/private_message_view.rs b/crates/db_views_actor/src/private_message_view.rs deleted file mode 100644 index 2345e7466..000000000 --- a/crates/db_views_actor/src/private_message_view.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::structs::PrivateMessageView; -use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - aliases, - newtypes::PrivateMessageId, - schema::{instance_actions, person, person_actions, private_message}, - utils::{actions, get_conn, DbPool}, -}; - -impl PrivateMessageView { - pub async fn read( - pool: &mut DbPool<'_>, - private_message_id: PrivateMessageId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - - private_message::table - .find(private_message_id) - .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .inner_join( - aliases::person1.on(private_message::recipient_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, - )) - .select(( - private_message::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - )) - .first(conn) - .await - } -} diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs deleted file mode 100644 index b1f75c86d..000000000 --- a/crates/db_views_actor/src/structs.rs +++ /dev/null @@ -1,259 +0,0 @@ -#[cfg(feature = "full")] -use diesel::Queryable; -use lemmy_db_schema::{ - aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates, PostAggregates}, - source::{ - comment::Comment, - comment_reply::CommentReply, - community::Community, - images::ImageDetails, - person::Person, - person_comment_mention::PersonCommentMention, - person_post_mention::PersonPostMention, - post::Post, - private_message::PrivateMessage, - }, - SubscribedType, -}; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -#[cfg(feature = "full")] -use ts_rs::TS; - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A community follower. -pub struct CommunityFollowerView { - pub community: Community, - pub follower: Person, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A community moderator. -pub struct CommunityModeratorView { - pub community: Community, - pub moderator: Person, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -/// A community person ban. -pub struct CommunityPersonBanView { - pub community: Community, - pub person: Person, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A community view. -pub struct CommunityView { - pub community: Community, - pub subscribed: SubscribedType, - pub blocked: bool, - pub counts: CommunityAggregates, - pub banned_from_community: bool, -} - -/// The community sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -pub enum CommunitySortType { - #[default] - Active, - Hot, - New, - Old, - TopDay, - TopWeek, - TopMonth, - TopYear, - TopAll, - MostComments, - NewComments, - TopHour, - TopSixHour, - TopTwelveHour, - TopThreeMonths, - TopSixMonths, - TopNineMonths, - Controversial, - Scaled, - NameAsc, - NameDesc, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A person comment mention view. -pub struct PersonCommentMentionView { - pub person_comment_mention: PersonCommentMention, - pub comment: Comment, - pub creator: Person, - pub post: Post, - pub community: Community, - pub recipient: Person, - pub counts: CommentAggregates, - pub creator_banned_from_community: bool, - pub banned_from_community: bool, - pub creator_is_moderator: bool, - pub creator_is_admin: bool, - pub subscribed: SubscribedType, - pub saved: bool, - pub creator_blocked: bool, - #[cfg_attr(feature = "full", ts(optional))] - pub my_vote: Option, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A person post mention view. -pub struct PersonPostMentionView { - pub person_post_mention: PersonPostMention, - pub post: Post, - pub creator: Person, - pub community: Community, - #[cfg_attr(feature = "full", ts(optional))] - pub image_details: Option, - pub recipient: Person, - pub counts: PostAggregates, - pub creator_banned_from_community: bool, - pub banned_from_community: bool, - pub creator_is_moderator: bool, - pub creator_is_admin: bool, - pub subscribed: SubscribedType, - pub saved: bool, - pub read: bool, - pub hidden: bool, - pub creator_blocked: bool, - #[cfg_attr(feature = "full", ts(optional))] - pub my_vote: Option, - pub unread_comments: i64, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A comment reply view. -pub struct CommentReplyView { - pub comment_reply: CommentReply, - pub comment: Comment, - pub creator: Person, - pub post: Post, - pub community: Community, - pub recipient: Person, - pub counts: CommentAggregates, - pub creator_banned_from_community: bool, - pub banned_from_community: bool, - pub creator_is_moderator: bool, - pub creator_is_admin: bool, - pub subscribed: SubscribedType, - pub saved: bool, - pub creator_blocked: bool, - #[cfg_attr(feature = "full", ts(optional))] - pub my_vote: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A person view. -pub struct PersonView { - pub person: Person, - pub counts: PersonAggregates, - pub is_admin: bool, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -pub struct PendingFollow { - pub person: Person, - pub community: Community, - pub is_new_instance: bool, - pub subscribed: SubscribedType, -} - -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A private message view. -pub struct PrivateMessageView { - pub private_message: PrivateMessage, - pub creator: Person, - pub recipient: Person, -} - -/// like PaginationCursor but for the report_combined table -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -pub struct InboxCombinedPaginationCursor(pub String); - -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -/// A combined inbox view -pub struct InboxCombinedViewInternal { - // Comment reply - pub comment_reply: Option, - // Person comment mention - pub person_comment_mention: Option, - // Person post mention - pub person_post_mention: Option, - pub post_counts: Option, - pub post_unread_comments: Option, - pub post_saved: bool, - pub post_read: bool, - pub post_hidden: bool, - pub my_post_vote: Option, - pub image_details: Option, - // Private message - pub private_message: Option, - // Shared - pub post: Option, - pub community: Option, - pub comment: Option, - pub comment_counts: Option, - pub comment_saved: bool, - pub my_comment_vote: Option, - pub subscribed: SubscribedType, - pub item_creator: Person, - pub item_recipient: Person, - pub item_creator_is_admin: bool, - pub item_creator_is_moderator: bool, - pub item_creator_banned_from_community: bool, - pub item_creator_blocked: bool, - pub banned_from_community: bool, -} - -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -// Use serde's internal tagging, to work easier with javascript libraries -#[serde(tag = "type_")] -pub enum InboxCombinedView { - CommentReply(CommentReplyView), - CommentMention(PersonCommentMentionView), - PostMention(PersonPostMentionView), - PrivateMessage(PrivateMessageView), -} diff --git a/crates/db_views_moderator/Cargo.toml b/crates/db_views_moderator/Cargo.toml deleted file mode 100644 index a7257c4f1..000000000 --- a/crates/db_views_moderator/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "lemmy_db_views_moderator" -version.workspace = true -edition.workspace = true -description.workspace = true -license.workspace = true -homepage.workspace = true -documentation.workspace = true -repository.workspace = true - -[lib] -doctest = false - -[lints] -workspace = true - -[features] -full = [ - "lemmy_db_schema/full", - "lemmy_utils", - "i-love-jesus", - "diesel", - "diesel-async", - "ts-rs", -] - -[dependencies] -lemmy_db_schema = { workspace = true } -lemmy_utils = { workspace = true, optional = true } -i-love-jesus = { workspace = true, optional = true } -diesel = { workspace = true, features = [ - "chrono", - "postgres", - "serde_json", -], optional = true } -diesel-async = { workspace = true, features = [ - "deadpool", - "postgres", -], optional = true } -serde = { workspace = true } -serde_with = { workspace = true } -ts-rs = { workspace = true, optional = true } - -[dev-dependencies] -serial_test = { workspace = true } -tokio = { workspace = true } -pretty_assertions = { workspace = true } diff --git a/crates/db_views_moderator/src/lib.rs b/crates/db_views_moderator/src/lib.rs deleted file mode 100644 index 1cc21da27..000000000 --- a/crates/db_views_moderator/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[cfg(feature = "full")] -pub mod modlog_combined_view; -pub mod structs; diff --git a/crates/db_views_moderator/src/structs.rs b/crates/db_views_moderator/src/structs.rs deleted file mode 100644 index 513a79705..000000000 --- a/crates/db_views_moderator/src/structs.rs +++ /dev/null @@ -1,332 +0,0 @@ -#[cfg(feature = "full")] -use diesel::Queryable; -use lemmy_db_schema::source::{ - comment::Comment, - community::Community, - 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, -}; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -#[cfg(feature = "full")] -use ts_rs::TS; - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 someone is added as a community moderator. -pub struct ModAddCommunityView { - pub mod_add_community: ModAddCommunity, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub community: Community, - pub other_person: Person, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 someone is added as a site moderator. -pub struct ModAddView { - pub mod_add: ModAdd, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub other_person: Person, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 someone is banned from a community. -pub struct ModBanFromCommunityView { - pub mod_ban_from_community: ModBanFromCommunity, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub community: Community, - pub other_person: Person, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 someone is banned from the site. -pub struct ModBanView { - pub mod_ban: ModBan, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub other_person: Person, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 a community is hidden from public view. -pub struct ModHideCommunityView { - pub mod_hide_community: ModHideCommunity, - #[cfg_attr(feature = "full", ts(optional))] - pub admin: Option, - pub community: Community, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 a moderator locks a post (prevents new comments being made). -pub struct ModLockPostView { - pub mod_lock_post: ModLockPost, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub other_person: Person, - pub post: Post, - pub community: Community, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 a moderator removes a comment. -pub struct ModRemoveCommentView { - pub mod_remove_comment: ModRemoveComment, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub other_person: Person, - pub comment: Comment, - pub post: Post, - pub community: Community, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 a moderator removes a community. -pub struct ModRemoveCommunityView { - pub mod_remove_community: ModRemoveCommunity, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub community: Community, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 a moderator removes a post. -pub struct ModRemovePostView { - pub mod_remove_post: ModRemovePost, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub other_person: Person, - pub post: Post, - pub community: Community, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 a moderator features a post on a community (pins it to the top). -pub struct ModFeaturePostView { - pub mod_feature_post: ModFeaturePost, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub other_person: Person, - pub post: Post, - pub community: Community, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 a moderator transfers a community to a new owner. -pub struct ModTransferCommunityView { - pub mod_transfer_community: ModTransferCommunity, - #[cfg_attr(feature = "full", ts(optional))] - pub moderator: Option, - pub community: Community, - pub other_person: Person, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 comment. -pub struct AdminPurgeCommentView { - pub admin_purge_comment: AdminPurgeComment, - #[cfg_attr(feature = "full", ts(optional))] - pub admin: Option, - pub post: Post, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 community. -pub struct AdminPurgeCommunityView { - pub admin_purge_community: AdminPurgeCommunity, - #[cfg_attr(feature = "full", ts(optional))] - pub admin: Option, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 person. -pub struct AdminPurgePersonView { - pub admin_purge_person: AdminPurgePerson, - #[cfg_attr(feature = "full", ts(optional))] - pub admin: Option, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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 AdminPurgePostView { - pub admin_purge_post: AdminPurgePost, - #[cfg_attr(feature = "full", ts(optional))] - pub admin: Option, - pub community: Community, -} - -#[skip_serializing_none] -#[derive(Debug, PartialEq, 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, PartialEq, 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_allow_instance: AdminAllowInstance, - pub instance: Instance, - #[cfg_attr(feature = "full", ts(optional))] - pub admin: Option, -} - -/// like PaginationCursor but for the modlog_combined -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -pub struct ModlogCombinedPaginationCursor(pub String); - -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -/// A combined modlog view -pub struct ModlogCombinedViewInternal { - // Specific - pub admin_allow_instance: Option, - pub admin_block_instance: Option, - pub admin_purge_comment: Option, - pub admin_purge_community: Option, - pub admin_purge_person: Option, - pub admin_purge_post: Option, - pub mod_add: Option, - pub mod_add_community: Option, - pub mod_ban: Option, - pub mod_ban_from_community: Option, - pub mod_feature_post: Option, - pub mod_hide_community: Option, - pub mod_lock_post: Option, - pub mod_remove_comment: Option, - pub mod_remove_community: Option, - pub mod_remove_post: Option, - pub mod_transfer_community: Option, - // Specific fields - - // Shared - pub moderator: Option, - pub other_person: Option, - pub instance: Option, - pub community: Option, - pub post: Option, - pub comment: Option, -} - -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -// Use serde's internal tagging, to work easier with javascript libraries -#[serde(tag = "type_")] -pub enum ModlogCombinedView { - AdminAllowInstance(AdminAllowInstanceView), - AdminBlockInstance(AdminBlockInstanceView), - AdminPurgeComment(AdminPurgeCommentView), - AdminPurgeCommunity(AdminPurgeCommunityView), - AdminPurgePerson(AdminPurgePersonView), - AdminPurgePost(AdminPurgePostView), - ModAdd(ModAddView), - ModAddCommunity(ModAddCommunityView), - ModBan(ModBanView), - ModBanFromCommunity(ModBanFromCommunityView), - ModFeaturePost(ModFeaturePostView), - ModHideCommunity(ModHideCommunityView), - ModLockPost(ModLockPostView), - ModRemoveComment(ModRemoveCommentView), - ModRemoveCommunity(ModRemoveCommunityView), - ModRemovePost(ModRemovePostView), - ModTransferCommunity(ModTransferCommunityView), -} diff --git a/crates/federate/Cargo.toml b/crates/federate/Cargo.toml index 7ea46de80..5f51cb2c8 100644 --- a/crates/federate/Cargo.toml +++ b/crates/federate/Cargo.toml @@ -18,7 +18,7 @@ workspace = true lemmy_api_common.workspace = true lemmy_apub.workspace = true lemmy_db_schema = { workspace = true, features = ["full"] } -lemmy_db_views_actor.workspace = true +lemmy_db_views.workspace = true lemmy_utils.workspace = true activitypub_federation.workspace = true @@ -33,7 +33,6 @@ tokio = { workspace = true, features = ["full"] } tracing.workspace = true moka.workspace = true tokio-util = "0.7.13" -async-trait.workspace = true [dev-dependencies] serial_test = { workspace = true } @@ -41,5 +40,5 @@ url.workspace = true actix-web.workspace = true tracing-test = "0.2.5" uuid.workspace = true -test-context = "0.3.0" +test-context = "0.4.1" mockall = "0.13.1" diff --git a/crates/federate/src/inboxes.rs b/crates/federate/src/inboxes.rs index ec96b1d6c..bbcdf3f66 100644 --- a/crates/federate/src/inboxes.rs +++ b/crates/federate/src/inboxes.rs @@ -1,12 +1,11 @@ use crate::util::LEMMY_TEST_FAST_FEDERATION; -use async_trait::async_trait; use chrono::{DateTime, TimeZone, Utc}; use lemmy_db_schema::{ newtypes::{CommunityId, DbUrl, InstanceId}, source::{activity::SentActivity, site::Site}, utils::{ActualDbPool, DbPool}, }; -use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_db_views::structs::CommunityFollowerView; use lemmy_utils::error::LemmyResult; use reqwest::Url; use std::{ @@ -38,7 +37,6 @@ static FOLLOW_ADDITIONS_RECHECK_DELAY: LazyLock = LazyLock::n 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) -> LemmyResult; async fn get_instance_followed_community_inboxes( @@ -57,7 +55,6 @@ impl DbDataSource { } } -#[async_trait] impl DataSource for DbDataSource { 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 @@ -231,7 +228,6 @@ mod tests { use serde_json::json; mock! { DataSource {} - #[async_trait] impl DataSource for DataSource { async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> LemmyResult; async fn get_instance_followed_community_inboxes( @@ -284,7 +280,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site")?.into(), + ap_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, @@ -407,7 +403,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site")?.into(), + ap_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, @@ -524,7 +520,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site")?.into(), + ap_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, diff --git a/crates/federate/src/worker.rs b/crates/federate/src/worker.rs index 260103c4a..ae88e57c5 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -487,9 +487,9 @@ mod test { let instance = Instance::read_or_create(&mut context.pool(), "localhost".to_string()).await?; let actor_keypair = generate_actor_keypair()?; - let actor_id: DbUrl = Url::parse("http://local.com/u/alice")?.into(); + let ap_id: DbUrl = Url::parse("http://local.com/u/alice")?.into(); let person_form = PersonInsertForm { - actor_id: Some(actor_id.clone()), + ap_id: Some(ap_id.clone()), private_key: (Some(actor_keypair.private_key)), inbox_url: Some(generate_inbox_url()?), ..PersonInsertForm::new("alice".to_string(), actor_keypair.public_key, instance.id) @@ -571,7 +571,7 @@ mod test { tracing::debug!("received first stats"); assert_eq!(data.instance.id, rcv.state.instance_id); - let sent = send_activity(data.person.actor_id.clone(), &data.context, true).await?; + let sent = send_activity(data.person.ap_id.clone(), &data.context, true).await?; tracing::debug!("sent activity"); // receive for successfully sent activity let inbox_rcv = data.inbox_receiver.recv().await.unwrap(); @@ -614,7 +614,7 @@ mod test { // let last_id_before = rcv.state.last_successful_id.unwrap(); let mut sent = Vec::new(); for _ in 0..40 { - sent.push(send_activity(data.person.actor_id.clone(), &data.context, false).await?); + sent.push(send_activity(data.person.ap_id.clone(), &data.context, false).await?); } sleep(2 * *WORK_FINISHED_RECHECK_DELAY).await; tracing::debug!("sent activity"); @@ -643,7 +643,7 @@ mod test { tracing::debug!("sending {} activities", count); let mut sent = Vec::new(); for _ in 0..count { - sent.push(send_activity(data.person.actor_id.clone(), &data.context, false).await?); + sent.push(send_activity(data.person.ap_id.clone(), &data.context, false).await?); } sleep(2 * *WORK_FINISHED_RECHECK_DELAY).await; tracing::debug!("sent activity"); @@ -660,7 +660,7 @@ mod test { let form = InstanceForm::new(data.instance.domain.clone()); Instance::update(&mut data.context.pool(), data.instance.id, form).await?; - send_activity(data.person.actor_id.clone(), &data.context, true).await?; + send_activity(data.person.ap_id.clone(), &data.context, true).await?; data.inbox_receiver.recv().await.unwrap(); let instance = @@ -702,7 +702,7 @@ mod test { } async fn send_activity( - actor_id: DbUrl, + ap_id: DbUrl, context: &LemmyContext, wait: bool, ) -> LemmyResult { @@ -725,7 +725,7 @@ mod test { send_all_instances: false, send_community_followers_of: None, actor_type: ActorType::Person, - actor_apub_id: actor_id, + actor_apub_id: ap_id, }; let sent = SentActivity::create(&mut context.pool(), form).await?; diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index aa75dc12c..85a066964 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -18,7 +18,6 @@ workspace = true [dependencies] lemmy_utils = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true } -lemmy_db_views_actor = { workspace = true } lemmy_db_schema = { workspace = true } lemmy_api_common = { workspace = true, features = ["full"] } activitypub_federation = { workspace = true } @@ -32,5 +31,16 @@ serde = { workspace = true } url = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } +futures-util.workspace = true http.workspace = true +diesel.workspace = true +diesel-async.workspace = true +clokwerk = "0.4.0" +prometheus = { version = "0.13.4", features = ["process"] } rss = "2.0.11" +actix-web-prom = "0.9.0" +actix-cors = "0.7.0" + +[dev-dependencies] +pretty_assertions.workspace = true +serial_test.workspace = true diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index f723572dd..f917d7d06 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -1,8 +1,10 @@ -use crate::local_user_view_from_jwt; use actix_web::{error::ErrorBadRequest, web, Error, HttpRequest, HttpResponse, Result}; use anyhow::anyhow; use chrono::{DateTime, Utc}; -use lemmy_api_common::{context::LemmyContext, utils::check_private_instance}; +use lemmy_api_common::{ + context::LemmyContext, + utils::{check_private_instance, local_user_view_from_jwt}, +}; use lemmy_db_schema::{ source::{community::Community, person::Person}, traits::ApubActor, @@ -11,13 +13,14 @@ use lemmy_db_schema::{ PostSortType, }; use lemmy_db_views::{ - post_view::PostQuery, - structs::{PostView, SiteView}, + combined::inbox_combined_view::InboxCombinedQuery, + post::post_view::PostQuery, + structs::{InboxCombinedView, PostView, SiteView}, }; -use lemmy_db_views_actor::{inbox_combined_view::InboxCombinedQuery, structs::InboxCombinedView}; use lemmy_utils::{ cache_header::cache_1hour, error::{LemmyError, LemmyErrorType, LemmyResult}, + settings::structs::Settings, utils::markdown::markdown_to_html, }; use rss::{ @@ -88,7 +91,6 @@ static RSS_NAMESPACE: LazyLock> = LazyLock::new(|| { h }); -#[tracing::instrument(skip_all)] async fn get_all_feed( info: web::Query, context: web::Data, @@ -105,7 +107,6 @@ async fn get_all_feed( ) } -#[tracing::instrument(skip_all)] async fn get_local_feed( info: web::Query, context: web::Data, @@ -122,7 +123,6 @@ async fn get_local_feed( ) } -#[tracing::instrument(skip_all)] async fn get_feed_data( context: &LemmyContext, listing_type: ListingType, @@ -144,7 +144,7 @@ async fn get_feed_data( .list(&site_view.site, &mut context.pool()) .await?; - let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?; + let items = create_post_items(posts, context.settings())?; let mut channel = Channel { namespaces: RSS_NAMESPACE.clone(), @@ -166,7 +166,6 @@ async fn get_feed_data( ) } -#[tracing::instrument(skip_all)] async fn get_feed( req: HttpRequest, info: web::Query, @@ -227,7 +226,6 @@ async fn get_feed( ) } -#[tracing::instrument(skip_all)] async fn get_feed_user( context: &LemmyContext, sort_type: &PostSortType, @@ -253,11 +251,11 @@ async fn get_feed_user( .list(&site_view.site, &mut context.pool()) .await?; - let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?; + let items = create_post_items(posts, context.settings())?; let channel = Channel { namespaces: RSS_NAMESPACE.clone(), title: format!("{} - {}", site_view.site.name, person.name), - link: person.actor_id.to_string(), + link: person.ap_id.to_string(), items, ..Default::default() }; @@ -265,7 +263,6 @@ async fn get_feed_user( Ok(channel) } -#[tracing::instrument(skip_all)] async fn get_feed_community( context: &LemmyContext, sort_type: &PostSortType, @@ -293,12 +290,12 @@ async fn get_feed_community( .list(&site_view.site, &mut context.pool()) .await?; - let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?; + let items = create_post_items(posts, context.settings())?; let mut channel = Channel { namespaces: RSS_NAMESPACE.clone(), title: format!("{} - {}", site_view.site.name, community.name), - link: community.actor_id.to_string(), + link: community.ap_id.to_string(), items, ..Default::default() }; @@ -310,7 +307,6 @@ async fn get_feed_community( Ok(channel) } -#[tracing::instrument(skip_all)] async fn get_feed_front( context: &LemmyContext, sort_type: &PostSortType, @@ -335,7 +331,7 @@ async fn get_feed_front( .await?; let protocol_and_hostname = context.settings().get_protocol_and_hostname(); - let items = create_post_items(posts, &protocol_and_hostname)?; + let items = create_post_items(posts, context.settings())?; let mut channel = Channel { namespaces: RSS_NAMESPACE.clone(), title: format!("{} - Subscribed", site_view.site.name), @@ -351,7 +347,6 @@ async fn get_feed_front( Ok(channel) } -#[tracing::instrument(skip_all)] async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult { let site_view = SiteView::read_local(&mut context.pool()).await?; let local_user = local_user_view_from_jwt(jwt, context).await?; @@ -368,7 +363,7 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult LemmyResult, protocol_and_hostname: &str, + context: &LemmyContext, ) -> LemmyResult> { let reply_items: Vec = inbox .iter() .map(|r| match r { InboxCombinedView::CommentReply(v) => { - let reply_url = format!("{}/comment/{}", protocol_and_hostname, v.comment.id); + let reply_url = v.comment.local_url(context.settings())?; build_item( &v.creator.name, &v.comment.published, - &reply_url, + reply_url.as_str(), &v.comment.content, protocol_and_hostname, ) } InboxCombinedView::CommentMention(v) => { - let mention_url = format!("{}/comment/{}", protocol_and_hostname, v.comment.id); + let mention_url = v.comment.local_url(context.settings())?; build_item( &v.creator.name, &v.comment.published, - &mention_url, + mention_url.as_str(), &v.comment.content, protocol_and_hostname, ) } InboxCombinedView::PostMention(v) => { - let mention_url = format!("{}/post/{}", protocol_and_hostname, v.post.id); + let mention_url = v.post.local_url(context.settings())?; build_item( &v.creator.name, &v.post.published, - &mention_url, + mention_url.as_str(), &v.post.body.clone().unwrap_or_default(), protocol_and_hostname, ) @@ -439,7 +434,6 @@ fn create_reply_and_mention_items( Ok(reply_items) } -#[tracing::instrument(skip_all)] fn build_item( creator_name: &str, published: &DateTime, @@ -469,42 +463,46 @@ fn build_item( }) } -#[tracing::instrument(skip_all)] -fn create_post_items(posts: Vec, protocol_and_hostname: &str) -> LemmyResult> { +fn create_post_items(posts: Vec, settings: &Settings) -> LemmyResult> { let mut items: Vec = Vec::new(); for p in posts { - let post_url = format!("{}/post/{}", protocol_and_hostname, p.post.id); - let community_url = format!("{}/c/{}", protocol_and_hostname, &p.community.name); + let post_url = p.post.local_url(settings)?; + let community_url = Community::local_url(&p.community.name, settings)?; let dublin_core_ext = Some(DublinCoreExtension { - creators: vec![p.creator.actor_id.to_string()], + creators: vec![p.creator.ap_id.to_string()], ..DublinCoreExtension::default() }); let guid = Some(Guid { permalink: true, - value: post_url.clone(), + value: post_url.to_string(), }); let mut description = format!("submitted by {} to {}
{} points | {} comments", - p.creator.actor_id, + p.creator.ap_id, &p.creator.name, community_url, &p.community.name, - p.counts.score, + p.post.score, post_url, - p.counts.comments); + p.post.comments); // If its a url post, add it to the description // and see if we can parse it as a media enclosure. let enclosure_opt = p.post.url.map(|url| { - let link_html = format!("
{url}"); - description.push_str(&link_html); - let mime_type = p .post .url_content_type .unwrap_or_else(|| "application/octet-stream".to_string()); - let mut enclosure_bld = EnclosureBuilder::default(); + // If the url directly links to an image, wrap it in an tag for display. + let link_html = if mime_type.starts_with("image/") { + format!("
") + } else { + format!("
{url}") + }; + description.push_str(&link_html); + + let mut enclosure_bld = EnclosureBuilder::default(); enclosure_bld.url(url.as_str().to_string()); enclosure_bld.mime_type(mime_type); enclosure_bld.length("0".to_string()); @@ -535,17 +533,17 @@ fn create_post_items(posts: Vec, protocol_and_hostname: &str) -> Lemmy } let category = Category { name: p.community.title, - domain: Some(p.community.actor_id.to_string()), + domain: Some(p.community.ap_id.to_string()), }; let i = Item { title: Some(p.post.name), pub_date: Some(p.post.published.to_rfc2822()), - comments: Some(post_url.clone()), + comments: Some(post_url.to_string()), guid, description: Some(description), dublin_core_ext, - link: Some(post_url.clone()), + link: Some(post_url.to_string()), extensions, enclosure: enclosure_opt, categories: vec![category], diff --git a/crates/routes/src/images/download.rs b/crates/routes/src/images/download.rs index 76f09a8d1..c4317c4dd 100644 --- a/crates/routes/src/images/download.rs +++ b/crates/routes/src/images/download.rs @@ -125,5 +125,5 @@ pub(super) async fn do_get_image( pub(super) fn file_type(file_type: Option, name: &str) -> String { file_type .clone() - .unwrap_or_else(|| name.split('.').last().unwrap_or("jpg").to_string()) + .unwrap_or_else(|| name.split('.').next_back().unwrap_or("jpg").to_string()) } diff --git a/crates/routes/src/images/upload.rs b/crates/routes/src/images/upload.rs index 6ddc08458..8ceb6c22e 100644 --- a/crates/routes/src/images/upload.rs +++ b/crates/routes/src/images/upload.rs @@ -174,10 +174,13 @@ pub async fn do_upload_image( context: &Data, ) -> LemmyResult { let pictrs = context.settings().pictrs()?; + let max_upload_size = pictrs.max_upload_size.map(|m| m.to_string()); let image_url = format!("{}image", pictrs.url); let mut client_req = adapt_request(&req, image_url, context); + // Set pictrs parameters to downscale images and restrict file types. + // https://git.asonix.dog/asonix/pict-rs/#api client_req = match upload_type { Avatar => { let max_size = pictrs.max_avatar_size.to_string(); @@ -195,7 +198,13 @@ pub async fn do_upload_image( ("allow_video", "false"), ]) } - _ => client_req, + Other => { + let mut query = vec![("allow_video", pictrs.allow_video_uploads.to_string())]; + if let Some(max_upload_size) = max_upload_size { + query.push(("resize", max_upload_size)); + } + client_req.query(&query) + } }; if let Some(addr) = req.head().peer_addr { client_req = client_req.header("X-Forwarded-For", addr.to_string()) diff --git a/crates/routes/src/lib.rs b/crates/routes/src/lib.rs index a88225622..1a9b71028 100644 --- a/crates/routes/src/lib.rs +++ b/crates/routes/src/lib.rs @@ -1,17 +1,6 @@ -use lemmy_api_common::{claims::Claims, context::LemmyContext, utils::check_user_valid}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyResult; - pub mod feeds; pub mod images; +pub mod middleware; pub mod nodeinfo; +pub mod utils; pub mod webfinger; - -#[tracing::instrument(skip_all)] -async fn local_user_view_from_jwt(jwt: &str, context: &LemmyContext) -> LemmyResult { - let local_user_id = Claims::validate(jwt, context).await?; - let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; - check_user_valid(&local_user_view.person)?; - - Ok(local_user_view) -} diff --git a/crates/routes/src/middleware/idempotency.rs b/crates/routes/src/middleware/idempotency.rs new file mode 100644 index 000000000..2598cd12e --- /dev/null +++ b/crates/routes/src/middleware/idempotency.rs @@ -0,0 +1,176 @@ +use actix_web::{ + body::EitherBody, + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + http::Method, + Error, + HttpMessage, + HttpResponse, +}; +use futures_util::future::LocalBoxFuture; +use lemmy_api_common::lemmy_db_views::structs::LocalUserView; +use lemmy_db_schema::newtypes::LocalUserId; +use lemmy_utils::rate_limit::rate_limiter::InstantSecs; +use std::{ + collections::HashSet, + future::{ready, Ready}, + hash::{Hash, Hasher}, + sync::{Arc, RwLock}, + time::Duration, +}; + +/// https://www.ietf.org/archive/id/draft-ietf-httpapi-idempotency-key-header-01.html +const IDEMPOTENCY_HEADER: &str = "Idempotency-Key"; + +/// Delete idempotency keys older than this +const CLEANUP_INTERVAL_SECS: u32 = 120; + +#[derive(Debug)] +struct Entry { + user_id: LocalUserId, + key: String, + // Creation time is ignored for Eq, Hash and only used to cleanup old entries + created: InstantSecs, +} + +impl PartialEq for Entry { + fn eq(&self, other: &Self) -> bool { + self.user_id == other.user_id && self.key == other.key + } +} +impl Eq for Entry {} + +impl Hash for Entry { + fn hash(&self, state: &mut H) { + self.user_id.hash(state); + self.key.hash(state); + } +} + +#[derive(Clone)] +pub struct IdempotencySet { + set: Arc>>, +} + +impl Default for IdempotencySet { + fn default() -> Self { + let set: Arc>> = Default::default(); + + let set_ = set.clone(); + tokio::spawn(async move { + let interval = Duration::from_secs(CLEANUP_INTERVAL_SECS.into()); + let state_weak_ref = Arc::downgrade(&set_); + + // Run at every interval to delete entries older than the interval. + // This loop stops when all other references to `state` are dropped. + while let Some(state) = state_weak_ref.upgrade() { + tokio::time::sleep(interval).await; + let now = InstantSecs::now(); + #[allow(clippy::expect_used)] + let mut lock = state.write().expect("lock failed"); + lock.retain(|e| e.created.secs > now.secs.saturating_sub(CLEANUP_INTERVAL_SECS)); + lock.shrink_to_fit(); + } + }); + Self { set } + } +} + +pub struct IdempotencyMiddleware { + idempotency_set: IdempotencySet, +} + +impl IdempotencyMiddleware { + pub fn new(idempotency_set: IdempotencySet) -> Self { + Self { idempotency_set } + } +} + +impl Transform for IdempotencyMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type InitError = (); + type Transform = IdempotencyService; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(IdempotencyService { + service, + idempotency_set: self.idempotency_set.clone(), + })) + } +} + +pub struct IdempotencyService { + service: S, + idempotency_set: IdempotencySet, +} + +impl Service for IdempotencyService +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + #[allow(clippy::expect_used)] + fn call(&self, req: ServiceRequest) -> Self::Future { + let is_post_or_put = req.method() == Method::POST || req.method() == Method::PUT; + let idempotency = req + .headers() + .get(IDEMPOTENCY_HEADER) + .map(|i| i.to_str().unwrap_or_default().to_string()) + // Ignore values longer than 32 chars + .and_then(|i| (i.len() <= 32).then_some(i)) + // Only use idempotency for POST and PUT requests + .and_then(|i| is_post_or_put.then_some(i)); + + let user_id = { + let ext = req.extensions(); + ext.get().map(|u: &LocalUserView| u.local_user.id) + }; + + if let (Some(key), Some(user_id)) = (idempotency, user_id) { + let value = Entry { + user_id, + key, + created: InstantSecs::now(), + }; + if self + .idempotency_set + .set + .read() + .expect("lock failed") + .contains(&value) + { + // Duplicate request, return error + let (req, _pl) = req.into_parts(); + let response = HttpResponse::UnprocessableEntity() + .finish() + .map_into_right_body(); + return Box::pin(async { Ok(ServiceResponse::new(req, response)) }); + } else { + // New request, store key and continue + self + .idempotency_set + .set + .write() + .expect("lock failed") + .insert(value); + } + } + + let fut = self.service.call(req); + + Box::pin(async move { fut.await.map(ServiceResponse::map_into_left_body) }) + } +} diff --git a/crates/routes/src/middleware/mod.rs b/crates/routes/src/middleware/mod.rs new file mode 100644 index 000000000..c72709005 --- /dev/null +++ b/crates/routes/src/middleware/mod.rs @@ -0,0 +1,2 @@ +pub mod idempotency; +pub mod session; diff --git a/src/session_middleware.rs b/crates/routes/src/middleware/session.rs similarity index 89% rename from src/session_middleware.rs rename to crates/routes/src/middleware/session.rs index 7e0f38a4e..7019a64d5 100644 --- a/src/session_middleware.rs +++ b/crates/routes/src/middleware/session.rs @@ -7,8 +7,10 @@ use actix_web::{ }; use core::future::Ready; use futures_util::future::LocalBoxFuture; -use lemmy_api::{local_user_view_from_jwt, read_auth_token}; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{ + context::LemmyContext, + utils::{local_user_view_from_jwt, read_auth_token}, +}; use std::{future::ready, rc::Rc}; #[derive(Clone)] @@ -67,9 +69,8 @@ where if let Some(jwt) = &jwt { // Ignore any invalid auth so the site can still be used - // TODO: this means it will be impossible to get any error message for invalid jwt. Need - // to add a separate endpoint for that. - // https://github.com/LemmyNet/lemmy/issues/3702 + // This means it is be impossible to get any error message for invalid jwt. Need + // to use `/api/v4/account/validate_auth` for that. let local_user_view = local_user_view_from_jwt(jwt, &context).await.ok(); if let Some(local_user_view) = local_user_view { req.extensions_mut().insert(local_user_view); @@ -99,9 +100,8 @@ where #[cfg(test)] mod tests { - use crate::tests::test_context; use actix_web::test::TestRequest; - use lemmy_api_common::claims::Claims; + use lemmy_api_common::{claims::Claims, context::LemmyContext}; use lemmy_db_schema::{ source::{ instance::Instance, @@ -117,7 +117,7 @@ mod tests { #[tokio::test] #[serial] async fn test_session_auth() -> LemmyResult<()> { - let context = test_context().await; + let context = LemmyContext::init_test_context().await; let inserted_instance = Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?; diff --git a/crates/routes/src/nodeinfo.rs b/crates/routes/src/nodeinfo.rs index e5b183a0b..b6401d523 100644 --- a/crates/routes/src/nodeinfo.rs +++ b/crates/routes/src/nodeinfo.rs @@ -59,12 +59,12 @@ async fn node_info(context: web::Data) -> Result LemmyResult<()> { let protocol_and_hostname = &settings.get_protocol_and_hostname(); - user_updates_2020_04_02(pool, protocol_and_hostname).await?; - community_updates_2020_04_02(pool, protocol_and_hostname).await?; - post_updates_2020_04_03(pool, protocol_and_hostname).await?; - comment_updates_2020_04_03(pool, protocol_and_hostname).await?; - private_message_updates_2020_05_05(pool, protocol_and_hostname).await?; + user_updates_2020_04_02(pool, settings).await?; + community_updates_2020_04_02(pool, settings).await?; + post_updates_2020_04_03(pool, settings).await?; + comment_updates_2020_04_03(pool, settings).await?; + private_message_updates_2020_05_05(pool, settings).await?; post_thumbnail_url_updates_2020_07_27(pool, protocol_and_hostname).await?; apub_columns_2021_02_02(pool).await?; instance_actor_2022_01_28(pool, protocol_and_hostname).await?; @@ -55,18 +55,15 @@ pub async fn run_advanced_migrations( Ok(()) } -async fn user_updates_2020_04_02( - pool: &mut DbPool<'_>, - protocol_and_hostname: &str, -) -> LemmyResult<()> { - use lemmy_db_schema::schema::person::dsl::{actor_id, local, person}; +async fn user_updates_2020_04_02(pool: &mut DbPool<'_>, settings: &Settings) -> LemmyResult<()> { + use lemmy_db_schema::schema::person::dsl::{ap_id, local, person}; let conn = &mut get_conn(pool).await?; info!("Running user_updates_2020_04_02"); - // Update the actor_id, private_key, and public_key, last_refreshed_at + // Update the ap_id, private_key, and public_key, last_refreshed_at let incorrect_persons = person - .filter(actor_id.like("http://changeme%")) + .filter(ap_id.like("http://changeme%")) .filter(local.eq(true)) .load::(conn) .await?; @@ -75,11 +72,7 @@ async fn user_updates_2020_04_02( let keypair = generate_actor_keypair()?; let form = PersonUpdateForm { - actor_id: Some(generate_local_apub_endpoint( - EndpointType::Person, - &cperson.name, - protocol_and_hostname, - )?), + ap_id: Some(Person::local_url(&cperson.name, settings)?), private_key: Some(Some(keypair.private_key)), public_key: Some(keypair.public_key), last_refreshed_at: Some(Utc::now()), @@ -96,30 +89,26 @@ async fn user_updates_2020_04_02( async fn community_updates_2020_04_02( pool: &mut DbPool<'_>, - protocol_and_hostname: &str, + settings: &Settings, ) -> LemmyResult<()> { - use lemmy_db_schema::schema::community::dsl::{actor_id, community, local}; + use lemmy_db_schema::schema::community::dsl::{ap_id, community, local}; let conn = &mut get_conn(pool).await?; info!("Running community_updates_2020_04_02"); - // Update the actor_id, private_key, and public_key, last_refreshed_at + // Update the ap_id, private_key, and public_key, last_refreshed_at let incorrect_communities = community - .filter(actor_id.like("http://changeme%")) + .filter(ap_id.like("http://changeme%")) .filter(local.eq(true)) .load::(conn) .await?; for ccommunity in &incorrect_communities { let keypair = generate_actor_keypair()?; - let community_actor_id = generate_local_apub_endpoint( - EndpointType::Community, - &ccommunity.name, - protocol_and_hostname, - )?; + let community_ap_id = Community::local_url(&ccommunity.name, settings)?; let form = CommunityUpdateForm { - actor_id: Some(community_actor_id.clone()), + ap_id: Some(community_ap_id.clone()), private_key: Some(Some(keypair.private_key)), public_key: Some(keypair.public_key), last_refreshed_at: Some(Utc::now()), @@ -134,10 +123,7 @@ async fn community_updates_2020_04_02( Ok(()) } -async fn post_updates_2020_04_03( - pool: &mut DbPool<'_>, - protocol_and_hostname: &str, -) -> LemmyResult<()> { +async fn post_updates_2020_04_03(pool: &mut DbPool<'_>, settings: &Settings) -> LemmyResult<()> { use lemmy_db_schema::schema::post::dsl::{ap_id, local, post}; let conn = &mut get_conn(pool).await?; @@ -151,11 +137,7 @@ async fn post_updates_2020_04_03( .await?; for cpost in &incorrect_posts { - let apub_id = generate_local_apub_endpoint( - EndpointType::Post, - &cpost.id.to_string(), - protocol_and_hostname, - )?; + let apub_id = cpost.local_url(settings)?; Post::update( pool, cpost.id, @@ -172,10 +154,7 @@ async fn post_updates_2020_04_03( Ok(()) } -async fn comment_updates_2020_04_03( - pool: &mut DbPool<'_>, - protocol_and_hostname: &str, -) -> LemmyResult<()> { +async fn comment_updates_2020_04_03(pool: &mut DbPool<'_>, settings: &Settings) -> LemmyResult<()> { use lemmy_db_schema::schema::comment::dsl::{ap_id, comment, local}; let conn = &mut get_conn(pool).await?; @@ -189,11 +168,7 @@ async fn comment_updates_2020_04_03( .await?; for ccomment in &incorrect_comments { - let apub_id = generate_local_apub_endpoint( - EndpointType::Comment, - &ccomment.id.to_string(), - protocol_and_hostname, - )?; + let apub_id = ccomment.local_url(settings)?; Comment::update( pool, ccomment.id, @@ -212,7 +187,7 @@ async fn comment_updates_2020_04_03( async fn private_message_updates_2020_05_05( pool: &mut DbPool<'_>, - protocol_and_hostname: &str, + settings: &Settings, ) -> LemmyResult<()> { use lemmy_db_schema::schema::private_message::dsl::{ap_id, local, private_message}; let conn = &mut get_conn(pool).await?; @@ -227,11 +202,7 @@ async fn private_message_updates_2020_05_05( .await?; for cpm in &incorrect_pms { - let apub_id = generate_local_apub_endpoint( - EndpointType::PrivateMessage, - &cpm.id.to_string(), - protocol_and_hostname, - )?; + let apub_id = cpm.local_url(settings)?; PrivateMessage::update( pool, cpm.id, @@ -307,7 +278,7 @@ async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>) -> LemmyResult<()> { .await?; for c in &communities { - let followers_url_ = generate_followers_url(&c.actor_id)?; + let followers_url_ = generate_followers_url(&c.ap_id)?; let inbox_url_ = generate_inbox_url()?; diesel::update(community.find(c.id)) .set((followers_url.eq(followers_url_), inbox_url.eq(inbox_url_))) @@ -335,9 +306,9 @@ async fn instance_actor_2022_01_28( return Ok(()); } let key_pair = generate_actor_keypair()?; - let actor_id = Url::parse(protocol_and_hostname)?; + let ap_id = Url::parse(protocol_and_hostname)?; let site_form = SiteUpdateForm { - actor_id: Some(actor_id.clone().into()), + ap_id: Some(ap_id.clone().into()), last_refreshed_at: Some(Utc::now()), inbox_url: Some(generate_inbox_url()?), private_key: Some(Some(key_pair.private_key)), @@ -431,15 +402,11 @@ async fn initialize_local_site_2022_10_10( if let Some(setup) = &settings.setup { let person_keypair = generate_actor_keypair()?; - let person_actor_id = generate_local_apub_endpoint( - EndpointType::Person, - &setup.admin_username, - &settings.get_protocol_and_hostname(), - )?; + let person_ap_id = Person::local_url(&setup.admin_username, settings)?; // Register the user if there's a site setup let person_form = PersonInsertForm { - actor_id: Some(person_actor_id.clone()), + ap_id: Some(person_ap_id.clone()), inbox_url: Some(generate_inbox_url()?), private_key: Some(person_keypair.private_key), ..PersonInsertForm::new( @@ -460,7 +427,7 @@ async fn initialize_local_site_2022_10_10( // Add an entry for the site table let site_key_pair = generate_actor_keypair()?; - let site_actor_id = Url::parse(&settings.get_protocol_and_hostname())?; + let site_ap_id = Url::parse(&settings.get_protocol_and_hostname())?; let name = settings .setup @@ -468,7 +435,7 @@ async fn initialize_local_site_2022_10_10( .map(|s| s.site_name) .unwrap_or_else(|| "New Site".to_string()); let site_form = SiteInsertForm { - actor_id: Some(site_actor_id.clone().into()), + ap_id: Some(site_ap_id.clone().into()), last_refreshed_at: Some(Utc::now()), inbox_url: Some(generate_inbox_url()?), private_key: Some(site_key_pair.private_key), diff --git a/crates/routes/src/utils/mod.rs b/crates/routes/src/utils/mod.rs new file mode 100644 index 000000000..6833e82fe --- /dev/null +++ b/crates/routes/src/utils/mod.rs @@ -0,0 +1,30 @@ +use actix_cors::Cors; +use lemmy_utils::settings::structs::Settings; + +pub mod code_migrations; +pub mod prometheus_metrics; +pub mod scheduled_tasks; + +pub fn cors_config(settings: &Settings) -> Cors { + let self_origin = settings.get_protocol_and_hostname(); + let cors_origin_setting = settings.cors_origin(); + + let mut cors = Cors::default() + .allow_any_method() + .allow_any_header() + .expose_any_header() + .max_age(3600); + + if cfg!(debug_assertions) + || cors_origin_setting.is_empty() + || cors_origin_setting.contains(&"*".to_string()) + { + cors = cors.allow_any_origin(); + } else { + cors = cors.allowed_origin(&self_origin); + for c in cors_origin_setting { + cors = cors.allowed_origin(&c); + } + } + cors +} diff --git a/src/prometheus_metrics.rs b/crates/routes/src/utils/prometheus_metrics.rs similarity index 85% rename from src/prometheus_metrics.rs rename to crates/routes/src/utils/prometheus_metrics.rs index 512d63f38..858d58381 100644 --- a/src/prometheus_metrics.rs +++ b/crates/routes/src/utils/prometheus_metrics.rs @@ -1,10 +1,21 @@ use actix_web::{rt::System, web, App, HttpServer}; -use lemmy_api_common::context::LemmyContext; +use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder}; +use lemmy_api_common::{context::LemmyContext, LemmyErrorType}; use lemmy_utils::{error::LemmyResult, settings::structs::PrometheusConfig}; use prometheus::{default_registry, Encoder, Gauge, Opts, TextEncoder}; use std::{sync::Arc, thread}; use tracing::error; +/// Creates a middleware that populates http metrics for each path, method, and status code +pub fn new_prometheus_metrics() -> LemmyResult { + Ok( + PrometheusMetricsBuilder::new("lemmy_api") + .registry(default_registry().clone()) + .build() + .map_err(|e| LemmyErrorType::Unknown(format!("Should always be buildable: {e}")))?, + ) +} + struct PromContext { lemmy: LemmyContext, db_pool_metrics: DbPoolMetrics, diff --git a/src/scheduled_tasks.rs b/crates/routes/src/utils/scheduled_tasks.rs similarity index 89% rename from src/scheduled_tasks.rs rename to crates/routes/src/utils/scheduled_tasks.rs index 2a129a7d1..de7348b94 100644 --- a/src/scheduled_tasks.rs +++ b/crates/routes/src/utils/scheduled_tasks.rs @@ -1,3 +1,4 @@ +use crate::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use activitypub_federation::config::Data; use chrono::{DateTime, TimeZone, Utc}; use clokwerk::{AsyncScheduler, TimeUnits as CTimeUnits}; @@ -16,8 +17,8 @@ use diesel_async::{AsyncPgConnection, RunQueryDsl}; use lemmy_api_common::{ context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, + utils::send_webmention, }; -use lemmy_api_crud::post::create::send_webmention; use lemmy_db_schema::{ schema::{ captcha_answer, @@ -38,17 +39,8 @@ use lemmy_db_schema::{ post::{Post, PostUpdateForm}, }, traits::Crud, - utils::{ - find_action, - functions::coalesce, - get_conn, - now, - uplete, - DbPool, - DELETED_REPLACEMENT_TEXT, - }, + utils::{functions::coalesce, get_conn, now, uplete, DbPool, DELETED_REPLACEMENT_TEXT}, }; -use lemmy_routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use reqwest_middleware::ClientWithMiddleware; use std::time::Duration; @@ -215,17 +207,15 @@ async fn process_ranks_in_batches( // Raw `sql_query` is used as a performance optimization - Diesel does not support doing this // in a single query (neither as a CTE, nor using a subquery) let updated_rows = sql_query(format!( - r#"WITH batch AS (SELECT a.{id_column} - FROM {aggregates_table} a + r#"WITH batch AS (SELECT a.id + FROM {table_name} a WHERE a.published > $1 AND ({where_clause}) ORDER BY a.published LIMIT $2 FOR UPDATE SKIP LOCKED) - UPDATE {aggregates_table} a {set_clause} - FROM batch WHERE a.{id_column} = batch.{id_column} RETURNING a.published; + UPDATE {table_name} a {set_clause} + FROM batch WHERE a.id = batch.id RETURNING a.published; "#, - id_column = format_args!("{table_name}_id"), - aggregates_table = format_args!("{table_name}_aggregates"), )) .bind::(previous_batch_last_published) .bind::(update_batch_size) @@ -255,25 +245,30 @@ async fn process_post_aggregates_ranks_in_batches(conn: &mut AsyncPgConnection) let mut previous_batch_result = Some(process_start_time); while let Some(previous_batch_last_published) = previous_batch_result { let updated_rows = sql_query( - r#"WITH batch AS (SELECT pa.post_id - FROM post_aggregates pa - WHERE pa.published > $1 - AND (pa.hot_rank != 0 OR pa.hot_rank_active != 0) - ORDER BY pa.published - LIMIT $2 - FOR UPDATE SKIP LOCKED) - UPDATE post_aggregates pa - SET hot_rank = r.hot_rank(pa.score, pa.published), - hot_rank_active = r.hot_rank(pa.score, pa.newest_comment_time_necro), - scaled_rank = r.scaled_rank(pa.score, pa.published, ca.users_active_month) - FROM batch, community_aggregates ca - WHERE pa.post_id = batch.post_id and pa.community_id = ca.community_id RETURNING pa.published; - "#, + r#"WITH batch AS (SELECT pa.id + FROM post pa + WHERE pa.published > $1 + AND (pa.hot_rank != 0 OR pa.hot_rank_active != 0) + ORDER BY pa.published + LIMIT $2 + FOR UPDATE SKIP LOCKED) + UPDATE post pa + SET hot_rank = r.hot_rank(pa.score, pa.published), + hot_rank_active = r.hot_rank(pa.score, pa.newest_comment_time_necro), + scaled_rank = r.scaled_rank(pa.score, pa.published, ca.interactions_month) + FROM batch, community ca + WHERE pa.id = batch.id + AND pa.community_id = ca.id + RETURNING pa.published; +"#, ) .bind::(previous_batch_last_published) .bind::(update_batch_size) .get_results::(conn) - .await.map_err(|e| LemmyErrorType::Unknown(format!("Failed to update {} hot_ranks: {}", "post_aggregates", e)))?; + .await + .map_err(|e| { + LemmyErrorType::Unknown(format!("Failed to update post_aggregates hot_ranks: {}", e)) + })?; processed_rows_count += updated_rows.len(); previous_batch_result = updated_rows.last().map(|row| row.published); @@ -371,15 +366,20 @@ async fn active_counts(pool: &mut DbPool<'_>) -> LemmyResult<()> { for (full_form, abbr) in &intervals { let update_site_stmt = format!( - "update site_aggregates set users_active_{} = (select * from r.site_aggregates_activity('{}')) where site_id = 1", + "update local_site set users_active_{} = (select r.site_aggregates_activity('{}')) where site_id = 1", abbr, full_form ); sql_query(update_site_stmt).execute(&mut conn).await?; - 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); + let update_community_stmt = format!("update community ca set users_active_{} = mv.count_ from r.community_aggregates_activity('{}') mv where ca.id = mv.community_id_", abbr, full_form); sql_query(update_community_stmt).execute(&mut conn).await?; } + let update_interactions_stmt = "update community ca set interactions_month = mv.count_ from r.community_aggregates_interactions('1 month') mv where ca.id = mv.community_id_"; + sql_query(update_interactions_stmt) + .execute(&mut conn) + .await?; + info!("Done."); Ok(()) } @@ -425,6 +425,10 @@ async fn publish_scheduled_posts(context: &Data) -> LemmyResult<() let pool = &mut context.pool(); let mut conn = get_conn(pool).await?; + let not_banned_action = community_actions::table + .find((person::id, community::id)) + .filter(community_actions::received_ban.is_not_null()); + let scheduled_posts: Vec<_> = post::table .inner_join(community::table) .inner_join(person::table) @@ -436,10 +440,7 @@ async fn publish_scheduled_posts(context: &Data) -> LemmyResult<() .filter(not(person::banned.or(person::deleted))) .filter(not(community::removed.or(community::deleted))) // ensure that user isnt banned from community - .filter(not(exists(find_action( - community_actions::received_ban, - (person::id, community::id), - )))) + .filter(not(exists(not_banned_action))) .select((post::all_columns, community::all_columns)) .get_results::<(Post, Community)>(&mut conn) .await?; @@ -553,7 +554,6 @@ async fn build_update_instance_form( mod tests { use super::*; - use crate::{scheduled_tasks::build_update_instance_form, tests::test_context}; use lemmy_api_common::request::client_builder; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, @@ -586,7 +586,7 @@ mod tests { #[tokio::test] #[serial] async fn test_scheduled_tasks_no_errors() -> LemmyResult<()> { - let context = test_context().await; + let context = LemmyContext::init_test_context().await; startup_jobs(&mut context.pool()).await?; update_instance_software(&mut context.pool(), context.client()).await?; diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs index a20a786f9..f223d33a6 100644 --- a/crates/routes/src/webfinger.rs +++ b/crates/routes/src/webfinger.rs @@ -51,14 +51,14 @@ async fn get_webfinger_response( .await .ok() .flatten() - .map(|c| c.actor_id.into()); + .map(|c| c.ap_id.into()); let community_id: Option = Community::read_from_name(&mut context.pool(), name, false) .await .ok() .flatten() .and_then(|c| { if c.visibility == CommunityVisibility::Public { - let id: Url = c.actor_id.into(); + let id: Url = c.ap_id.into(); Some(id) } else { None diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index e6af866d5..66a0d2b81 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -72,17 +72,18 @@ uuid = { workspace = true, optional = true, features = ["v4"] } rosetta-i18n = { workspace = true, optional = true } tokio = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true } -html2text = { version = "0.12.6", optional = true } +html2text = { version = "0.14.0", optional = true } deser-hjson = { version = "2.2.4", optional = true } smart-default = { version = "0.7.1", optional = true } -lettre = { version = "0.11.11", default-features = false, features = [ +lettre = { version = "0.11.12", default-features = false, features = [ "builder", "smtp-transport", "tokio1-rustls-tls", + "pool", ], optional = true } markdown-it = { version = "0.6.1", optional = true } ts-rs = { workspace = true, optional = true } -enum-map = { workspace = true, optional = true } +enum-map = { version = "2.7", optional = true } cfg-if = "1" clearurls = { version = "0.0.4", features = ["linkify"] } markdown-it-block-spoiler = "1.0.1" @@ -91,6 +92,7 @@ markdown-it-sup = "1.0.1" markdown-it-ruby = "1.0.1" markdown-it-footnote = "0.2.0" moka = { workspace = true, optional = true } +git-version = "0.3.9" [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/crates/utils/build.rs b/crates/utils/build.rs index b336f8c56..66b26b2b3 100644 --- a/crates/utils/build.rs +++ b/crates/utils/build.rs @@ -1,11 +1,33 @@ +use std::fs::read_dir; + fn main() -> Result<(), Box> { - rosetta_build::config() - .source("en", "translations/email/en.json") - .source("fi", "translations/email/fi.json") - .source("ko", "translations/email/ko.json") - .source("pt", "translations/email/pt.json") - .fallback("en") - .generate()?; + let mut config = rosetta_build::config(); + + for path in read_dir("translations/email/")? { + let path = path?.path(); + if let Some(name) = path.file_name() { + let mut lang = name.to_string_lossy().to_string().replace(".json", ""); + + // Rename Chinese simplified variant because there is no translation zh + if lang == "zh_Hans" { + lang = "zh".to_string(); + } + // Rosetta doesnt support these language variants. + if lang.contains('_') { + continue; + } + + let path = path.to_string_lossy(); + rosetta_build::config() + .source(&lang, path.clone()) + .fallback(&lang) + .generate()?; + + config = config.source(lang, path); + } + } + + config.fallback("en").generate()?; Ok(()) } diff --git a/crates/utils/src/email.rs b/crates/utils/src/email.rs index 46abb47ea..5f08d9ecb 100644 --- a/crates/utils/src/email.rs +++ b/crates/utils/src/email.rs @@ -5,12 +5,14 @@ use crate::{ use html2text; use lettre::{ message::{Mailbox, MultiPart}, - transport::smtp::{authentication::Credentials, extension::ClientId}, + transport::smtp::extension::ClientId, Address, AsyncTransport, Message, }; -use std::str::FromStr; +use rosetta_i18n::{Language, LanguageId}; +use std::{str::FromStr, sync::OnceLock}; +use translations::Lang; use uuid::Uuid; pub mod translations { @@ -26,24 +28,19 @@ pub async fn send_email( html: &str, settings: &Settings, ) -> LemmyResult<()> { + static MAILER: OnceLock = OnceLock::new(); let email_config = settings.email.clone().ok_or(LemmyErrorType::NoEmailSetup)?; - let domain = settings.hostname.clone(); - let (smtp_server, smtp_port) = { - let email_and_port = email_config.smtp_server.split(':').collect::>(); - let email = *email_and_port - .first() - .ok_or(LemmyErrorType::EmailRequired)?; - let port = email_and_port - .get(1) - .ok_or(LemmyErrorType::EmailSmtpServerNeedsAPort)? - .parse::()?; - - (email, port) - }; + #[expect(clippy::expect_used)] + let mailer = MAILER.get_or_init(|| { + AsyncSmtpTransport::from_url(&email_config.connection) + .expect("init email transport") + .hello_name(ClientId::Domain(settings.hostname.clone())) + .build() + }); // 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 plain_text = html2text::from_read(html.as_bytes(), usize::MAX)?; let smtp_from_address = &email_config.smtp_from_address; @@ -68,24 +65,6 @@ pub async fn send_email( )) .with_lemmy_type(LemmyErrorType::EmailSendFailed)?; - // don't worry about 'dangeous'. it's just that leaving it at the default configuration - // is bad. - - // Set the TLS - let mut builder = match email_config.tls_type.as_str() { - "starttls" => AsyncSmtpTransport::starttls_relay(smtp_server)?.port(smtp_port), - "tls" => AsyncSmtpTransport::relay(smtp_server)?.port(smtp_port), - _ => AsyncSmtpTransport::builder_dangerous(smtp_server).port(smtp_port), - }; - - // Set the creds if they exist - let smtp_password = email_config.smtp_password(); - if let (Some(username), Some(password)) = (email_config.smtp_login, smtp_password) { - builder = builder.credentials(Credentials::new(username, password)); - } - - let mailer = builder.hello_name(ClientId::Domain(domain)).build(); - mailer .send(email) .await @@ -93,3 +72,12 @@ pub async fn send_email( Ok(()) } + +#[allow(clippy::expect_used)] +pub fn lang_str_to_lang(lang: &str) -> Lang { + let lang_id = LanguageId::new(lang); + Lang::from_language_id(&lang_id).unwrap_or_else(|| { + let en = LanguageId::new("en"); + Lang::from_language_id(&en).expect("default language") + }) +} diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index d9b02cf5a..cb12cba43 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -1,6 +1,6 @@ use cfg_if::cfg_if; use serde::{Deserialize, Serialize}; -use std::{backtrace::Backtrace, fmt::Debug}; +use std::fmt::Debug; use strum::{Display, EnumIter}; #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)] @@ -58,6 +58,7 @@ pub enum LemmyErrorType { LanguageNotAllowed, CouldntUpdatePost, NoPostEditAllowed, + NsfwNotAllowed, EditPrivateMessageNotAllowed, SiteAlreadyExists, ApplicationQuestionRequired, @@ -73,7 +74,6 @@ pub enum LemmyErrorType { ObjectNotLocal, NoEmailSetup, LocalSiteNotSetup, - EmailSmtpServerNeedsAPort, InvalidEmailAddress(String), RateLimitError, InvalidName, @@ -157,6 +157,7 @@ pub enum LemmyErrorType { #[cfg_attr(feature = "full", ts(optional))] error: Option, }, + CouldntParsePaginationToken, } /// Federation related errors, these dont need to be translated. @@ -186,13 +187,14 @@ pub enum FederationError { CantDeleteSite, ObjectIsNotPublic, ObjectIsNotPrivate, + PlatformLackingPrivateCommunitySupport, Unreachable, } cfg_if! { if #[cfg(feature = "full")] { - use std::fmt; + use std::{fmt, backtrace::Backtrace}; pub type LemmyResult = Result; pub struct LemmyError { @@ -242,14 +244,10 @@ cfg_if! { impl actix_web::error::ResponseError for LemmyError { fn status_code(&self) -> actix_web::http::StatusCode { - if self.error_type == LemmyErrorType::IncorrectLogin { - return actix_web::http::StatusCode::UNAUTHORIZED; - } - if self.error_type == LemmyErrorType::NotFound { - return actix_web::http::StatusCode::NOT_FOUND; - } - match self.inner.downcast_ref::() { - Some(diesel::result::Error::NotFound) => actix_web::http::StatusCode::NOT_FOUND, + match self.error_type { + LemmyErrorType::IncorrectLogin => actix_web::http::StatusCode::UNAUTHORIZED, + LemmyErrorType::NotFound => actix_web::http::StatusCode::NOT_FOUND, + LemmyErrorType::RateLimitError => actix_web::http::StatusCode::TOO_MANY_REQUESTS, _ => actix_web::http::StatusCode::BAD_REQUEST, } } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 3367c91bb..4b74db0d3 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -13,11 +13,15 @@ cfg_if! { } pub mod error; +use git_version::git_version; use std::time::Duration; pub type ConnectionId = usize; -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const VERSION: &str = git_version!( + args = ["--tags", "--dirty=-modified"], + fallback = env!("CARGO_PKG_VERSION") +); pub const REQWEST_TIMEOUT: Duration = Duration::from_secs(10); @@ -26,6 +30,9 @@ pub const CACHE_DURATION_FEDERATION: Duration = Duration::from_millis(500); #[cfg(not(debug_assertions))] pub const CACHE_DURATION_FEDERATION: Duration = Duration::from_secs(60); +#[cfg(debug_assertions)] +pub const CACHE_DURATION_API: Duration = Duration::from_secs(0); +#[cfg(not(debug_assertions))] pub const CACHE_DURATION_API: Duration = Duration::from_secs(1); pub const MAX_COMMENT_DEPTH_LIMIT: usize = 50; diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index 68f248d6c..7fe239f79 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -13,9 +13,9 @@ static START_TIME: LazyLock = LazyLock::new(Instant::now); /// Smaller than `std::time::Instant` because it uses a smaller integer for seconds and doesn't /// store nanoseconds -#[derive(PartialEq, Debug, Clone, Copy)] +#[derive(PartialEq, Debug, Clone, Copy, Hash)] pub struct InstantSecs { - secs: u32, + pub secs: u32, } #[allow(clippy::expect_used)] diff --git a/crates/utils/src/request.rs b/crates/utils/src/request.rs index 5353edb4e..9a5133adb 100644 --- a/crates/utils/src/request.rs +++ b/crates/utils/src/request.rs @@ -1,6 +1,5 @@ use std::future::Future; -#[tracing::instrument(skip_all)] pub async fn retry(f: F) -> Result where F: Fn() -> Fut, @@ -9,7 +8,6 @@ where retry_custom(|| async { Ok((f)().await) }).await } -#[tracing::instrument(skip_all)] #[allow(clippy::expect_used)] async fn retry_custom(f: F) -> Result where diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index ecd0a9b55..240632f8d 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -2,7 +2,7 @@ use crate::{error::LemmyResult, location_info}; use anyhow::{anyhow, Context}; use deser_hjson::from_str; use regex::Regex; -use std::{env, fs, io::Error, sync::LazyLock}; +use std::{env, fs, sync::LazyLock}; use structs::{PictrsConfig, Settings}; use url::Url; @@ -39,7 +39,10 @@ impl Settings { /// `lemmy_db_schema/src/lib.rs::get_database_url_from_env()` /// Warning: Only call this once. pub(crate) fn init() -> LemmyResult { - let config = from_str::(&Self::read_config_file()?)?; + let path = + env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| DEFAULT_CONFIG_FILE.to_string()); + let plain = fs::read_to_string(path)?; + let config = from_str::(&plain)?; if config.hostname == "unset" { Err(anyhow!("Hostname variable is not set!").into()) } else { @@ -55,14 +58,6 @@ impl Settings { } } - fn get_config_location() -> String { - env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| DEFAULT_CONFIG_FILE.to_string()) - } - - fn read_config_file() -> Result { - fs::read_to_string(Self::get_config_location()) - } - /// Returns either "http" or "https", depending on tls_enabled setting pub fn get_protocol_string(&self) -> &'static str { if self.tls_enabled { @@ -109,3 +104,15 @@ impl Settings { fn pictrs_placeholder_url() -> Url { Url::parse("http://localhost:8080").expect("parse pictrs url") } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_load_config() -> LemmyResult<()> { + Settings::init()?; + Ok(()) + } +} diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index effd68a64..1ab5b64da 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -9,7 +9,7 @@ use std::{ use url::Url; #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] -#[serde(default)] +#[serde(default, deny_unknown_fields)] pub struct Settings { /// settings related to the postgresql database pub database: DatabaseConfig, @@ -44,17 +44,21 @@ pub struct Settings { // Prometheus configuration. #[doku(example = "Some(Default::default())")] pub prometheus: Option, - /// Sets a response Access-Control-Allow-Origin CORS header + /// Sets a response Access-Control-Allow-Origin CORS header. Can also be set via environment: + /// `LEMMY_CORS_ORIGIN=example.org,site.com` /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin #[doku(example = "lemmy.tld")] - cors_origin: Option, + cors_origin: Vec, + /// Print logs in JSON format. You can also disable ANSI colors in logs with env var `NO_COLOR`. + pub json_logging: bool, } impl Settings { - pub fn cors_origin(&self) -> Option { + pub fn cors_origin(&self) -> Vec { env::var("LEMMY_CORS_ORIGIN") .ok() - .or(self.cors_origin.clone()) + .map(|e| e.split(',').map(ToString::to_string).collect()) + .unwrap_or(self.cors_origin.clone()) } } @@ -91,15 +95,23 @@ pub struct PictrsConfig { #[default(512)] pub max_thumbnail_size: u32, - /// Maximum size for user avatar, community icon and site icon. + /// Maximum size for user avatar, community icon and site icon. Larger images are downscaled. #[default(512)] pub max_avatar_size: u32, - /// Maximum size for user, community and site banner. Larger images are downscaled to fit - /// into a square of this size. + /// Maximum size for user, community and site banner. Larger images are downscaled. #[default(1024)] pub max_banner_size: u32, + /// Maximum size for other uploads (e.g. post images or markdown embed images). Larger + /// images are downscaled. + #[doku(example = "1024")] + pub max_upload_size: Option, + + /// Whether users can upload videos as post image or markdown embed. + #[default(true)] + pub allow_video_uploads: bool, + /// Prevent users from uploading images for posts or embedding in markdown. Avatars, icons and /// banners can still be uploaded. #[default(false)] @@ -107,7 +119,6 @@ pub struct PictrsConfig { } #[derive(Debug, Deserialize, Serialize, Clone, Default, Document, PartialEq)] -#[serde(deny_unknown_fields)] pub enum PictrsImageMode { /// Leave images unchanged, don't generate any local thumbnails for post urls. Instead the /// Opengraph image is directly returned as thumbnail @@ -130,7 +141,7 @@ pub enum PictrsImageMode { } #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] -#[serde(default)] +#[serde(default, deny_unknown_fields)] pub struct DatabaseConfig { /// Configure the database by specifying URI pointing to a postgres instance /// @@ -151,34 +162,19 @@ pub struct DatabaseConfig { } #[derive(Debug, Deserialize, Serialize, Clone, Document, SmartDefault)] -#[serde(deny_unknown_fields)] +#[serde(default, deny_unknown_fields)] pub struct EmailConfig { - /// Hostname and port of the smtp server - #[doku(example = "localhost:25")] - pub smtp_server: String, - /// Login name for smtp server - pub smtp_login: Option, - /// Password to login to the smtp server - smtp_password: Option, - #[doku(example = "noreply@example.com")] + /// https://docs.rs/lettre/0.11.14/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url + #[default("smtp://localhost:25")] + #[doku(example = "smtps://user:pass@hostname:port")] + pub(crate) connection: String, /// Address to send emails from, eg "noreply@your-instance.com" - pub smtp_from_address: String, - /// Whether or not smtp connections should use tls. Can be none, tls, or starttls - #[default("none")] - #[doku(example = "none")] - pub tls_type: String, -} - -impl EmailConfig { - pub fn smtp_password(&self) -> Option { - std::env::var("LEMMY_SMTP_PASSWORD") - .ok() - .or(self.smtp_password.clone()) - } + #[doku(example = "noreply@example.com")] + pub(crate) smtp_from_address: String, } #[derive(Debug, Deserialize, Serialize, Clone, Default, Document)] -#[serde(deny_unknown_fields)] +#[serde(default, deny_unknown_fields)] pub struct SetupConfig { /// Username for the admin user #[doku(example = "admin")] @@ -195,7 +191,7 @@ pub struct SetupConfig { } #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] -#[serde(deny_unknown_fields)] +#[serde(default, deny_unknown_fields)] pub struct PrometheusConfig { // Address that the Prometheus metrics will be served on. #[default(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))] @@ -208,7 +204,7 @@ pub struct PrometheusConfig { } #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] -#[serde(default)] +#[serde(default, deny_unknown_fields)] // named federation"worker"config to disambiguate from the activitypub library configuration pub struct FederationWorkerConfig { /// Limit to the number of concurrent outgoing federation requests per target instance. diff --git a/crates/utils/src/utils/markdown/image_links.rs b/crates/utils/src/utils/markdown/image_links.rs index d22860f67..c6e90685e 100644 --- a/crates/utils/src/utils/markdown/image_links.rs +++ b/crates/utils/src/utils/markdown/image_links.rs @@ -1,6 +1,15 @@ -use super::{link_rule::Link, MARKDOWN_PARSER}; -use crate::settings::SETTINGS; -use markdown_it::{plugins::cmark::inline::image::Image, NodeValue}; +use super::link_rule::Link; +use crate::{settings::SETTINGS, utils::markdown::link_rule}; +use markdown_it::{ + parser::linkfmt::LinkFormatter, + plugins::cmark::{ + block::fence, + inline::{image, image::Image}, + }, + MarkdownIt, + NodeValue, +}; +use std::sync::LazyLock; use url::Url; use urlencoding::encode; @@ -55,7 +64,18 @@ pub fn markdown_find_links(src: &str) -> Vec<(usize, usize)> { // Walk the syntax tree to find positions of image or link urls fn find_urls(src: &str) -> Vec<(usize, usize)> { - let ast = MARKDOWN_PARSER.parse(src); + // Use separate markdown parser here, with most features disabled for faster parsing, + // and a dummy link formatter which doesnt normalize links. + static PARSER: LazyLock = LazyLock::new(|| { + let mut p = MarkdownIt::new(); + p.link_formatter = Box::new(NoopLinkFormatter {}); + image::add(&mut p); + fence::add(&mut p); + link_rule::add(&mut p); + p + }); + + let ast = PARSER.parse(src); let mut links_offsets = vec![]; ast.walk(|node, _depth| { if let Some(image) = node.cast::() { @@ -94,6 +114,25 @@ impl UrlAndTitle for Link { } } +/// markdown-it normalizes links by default, which breaks the link rewriting. So we use a dummy +/// formatter here which does nothing. Note this isnt actually used to render the markdown. +#[derive(Debug)] +struct NoopLinkFormatter; + +impl LinkFormatter for NoopLinkFormatter { + fn validate_link(&self, _url: &str) -> Option<()> { + Some(()) + } + + fn normalize_link(&self, url: &str) -> String { + url.to_owned() + } + + fn normalize_link_text(&self, url: &str) -> String { + url.to_owned() + } +} + #[cfg(test)] mod tests { @@ -107,6 +146,12 @@ mod tests { let links = find_urls::("![test](https://example.com)"); assert_eq!(vec![(8, 27)], links); + + let links = find_urls::("![ითხოვს](https://example.com/ითხოვს)"); + assert_eq!(vec![(22, 60)], links); + + let links = find_urls::("![test](https://example.com/%C3%A4%C3%B6%C3%BC.jpg)"); + assert_eq!(vec![(8, 50)], links); } #[test] @@ -152,7 +197,12 @@ mod tests { "custom emoji support", r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#, r#"![party-blob](https://lemmy-alpha/api/v4/image/proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# - ) + ), + ( + "image with special chars", + "ითხოვს ![ითხოვს](http://example.com/ითხოვს%C3%A4.jpg)", + "ითხოვს ![ითხოვს](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2F%E1%83%98%E1%83%97%E1%83%AE%E1%83%9D%E1%83%95%E1%83%A1%25C3%25A4.jpg)", + ), ]; tests.iter().for_each(|&(msg, input, expected)| { diff --git a/crates/utils/src/utils/markdown/link_rule.rs b/crates/utils/src/utils/markdown/link_rule.rs index 15edcd7b1..eeedd2b43 100644 --- a/crates/utils/src/utils/markdown/link_rule.rs +++ b/crates/utils/src/utils/markdown/link_rule.rs @@ -1,4 +1,12 @@ -use markdown_it::{generics::inline::full_link, MarkdownIt, Node, NodeValue, Renderer}; +use crate::utils::mention::MENTIONS_REGEX; +use markdown_it::{ + generics::inline::full_link, + parser::inline::Text, + MarkdownIt, + Node, + NodeValue, + Renderer, +}; /// Renders markdown links. Copied directly from markdown-it source, unlike original code it also /// sets `rel=nofollow` attribute. @@ -22,6 +30,14 @@ impl NodeValue for Link { attrs.push(("title", title.clone())); } + let text = node.children.first().and_then(|n| n.cast::()); + if let Some(text) = text { + if MENTIONS_REGEX.is_match(&text.content) { + attrs.push(("class", "u-url".to_string())); + attrs.push(("class", "mention".to_string())); + } + } + fmt.open("a", &attrs); fmt.contents(&node.children); fmt.close("a"); diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs index 3c761143c..8abbca4fc 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -49,7 +49,6 @@ mod tests { use super::*; use crate::utils::validation::check_urls_are_valid; - use image_links::markdown_rewrite_image_links; use pretty_assertions::assert_eq; use regex::escape; @@ -134,6 +133,11 @@ mod tests {
  • \n\

    example.com ↩︎

    \n\
  • \n\n\n" + ), + ( + "mention links", + "[@example@example.com](https://example.com/u/example)", + "

    @example@example.com

    \n" ) ]; @@ -148,63 +152,6 @@ mod tests { }); } - #[test] - fn test_markdown_proxy_images() { - let tests: Vec<_> = - vec![ - ( - "remote image proxied", - "![link](http://example.com/image.jpg)", - "![link](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", - ), - ( - "local image unproxied", - "![link](http://lemmy-alpha/image.jpg)", - "![link](http://lemmy-alpha/image.jpg)", - ), - ( - "multiple image links", - "![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)", - "![link](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", - ), - ( - "empty link handled", - "![image]()", - "![image]()" - ), - ( - "empty label handled", - "![](http://example.com/image.jpg)", - "![](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" - ), - ( - "invalid image link removed", - "![image](http-not-a-link)", - "![image]()" - ), - ( - "label with nested markdown handled", - "![a *b* c](http://example.com/image.jpg)", - "![a *b* c](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" - ), - ( - "custom emoji support", - r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#, - r#"![party-blob](https://lemmy-alpha/api/v4/image/proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# - ) - ]; - - tests.iter().for_each(|&(msg, input, expected)| { - let result = markdown_rewrite_image_links(input.to_string()); - - assert_eq!( - result.0, expected, - "Testing {}, with original input '{}'", - msg, input - ); - }); - } - // This replicates the logic when saving url blocklist patterns and querying them. // Refer to lemmy_api_crud::site::update::update_site and // lemmy_api_common::utils::get_url_blocklist(). diff --git a/crates/utils/src/utils/mention.rs b/crates/utils/src/utils/mention.rs index 0a011f848..dca9c35b4 100644 --- a/crates/utils/src/utils/mention.rs +++ b/crates/utils/src/utils/mention.rs @@ -3,7 +3,7 @@ use regex::Regex; use std::sync::LazyLock; #[allow(clippy::expect_used)] -static MENTIONS_REGEX: LazyLock = LazyLock::new(|| { +pub(crate) static MENTIONS_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"@(?P[\w.]+)@(?P[a-zA-Z0-9._:-]+)").expect("compile regex") }); // TODO nothing is done with community / group webfingers yet, so just ignore those for now diff --git a/crates/utils/src/utils/slurs.rs b/crates/utils/src/utils/slurs.rs index 8df7bc3d3..6d594d129 100644 --- a/crates/utils/src/utils/slurs.rs +++ b/crates/utils/src/utils/slurs.rs @@ -1,45 +1,25 @@ use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; -use regex::{Regex, RegexBuilder}; +use regex::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() - } +pub fn remove_slurs(test: &str, slur_regex: &Regex) -> String { + slur_regex.replace_all(test, "*removed*").to_string() } -pub(crate) fn slur_check<'a>( - test: &'a str, - slur_regex: &'a Option>, -) -> Result<(), Vec<&'a str>> { - if let Some(Ok(slur_regex)) = slur_regex { - let mut matches: Vec<&str> = slur_regex.find_iter(test).map(|mat| mat.as_str()).collect(); +pub(crate) fn slur_check<'a>(test: &'a str, slur_regex: &'a Regex) -> Result<(), Vec<&'a str>> { + let mut matches: Vec<&str> = slur_regex.find_iter(test).map(|mat| mat.as_str()).collect(); - // Unique - matches.sort_unstable(); - matches.dedup(); + // Unique + matches.sort_unstable(); + matches.dedup(); - if matches.is_empty() { - Ok(()) - } else { - Err(matches) - } - } else { + if matches.is_empty() { Ok(()) + } else { + Err(matches) } } -pub fn build_slur_regex(regex_str: Option<&str>) -> Option> { - regex_str.map(|slurs| { - RegexBuilder::new(slurs) - .case_insensitive(true) - .build() - .with_lemmy_type(LemmyErrorType::InvalidRegex) - }) -} - -pub fn check_slurs(text: &str, slur_regex: &Option>) -> LemmyResult<()> { +pub fn check_slurs(text: &str, slur_regex: &Regex) -> 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,10 +27,7 @@ pub fn check_slurs(text: &str, slur_regex: &Option>) -> Lemmy } } -pub fn check_slurs_opt( - text: &Option, - slur_regex: &Option>, -) -> LemmyResult<()> { +pub fn check_slurs_opt(text: &Option, slur_regex: &Regex) -> LemmyResult<()> { match text { Some(t) => check_slurs(t, slur_regex), None => Ok(()), @@ -67,7 +44,7 @@ pub(crate) fn slurs_vec_to_str(slurs: &[&str]) -> String { mod test { use crate::{ - error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, + error::LemmyResult, utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str}, }; use pretty_assertions::assert_eq; @@ -75,7 +52,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().with_lemmy_type(LemmyErrorType::InvalidRegex)); + let slur_regex = RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build()?; let test = "faggot test kike tranny cocksucker retardeds. Capitalized Niggerz. This is a bunch of other safe text."; let slur_free = "No slurs here"; diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index bc4155e6b..1147a0e21 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -221,31 +221,34 @@ 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>) -> Option> { +pub fn build_and_check_regex(regex_str_opt: Option<&str>) -> LemmyResult { + // Placeholder regex which doesnt match anything + // https://stackoverflow.com/a/940840 + let match_nothing = RegexBuilder::new("a^") + .build() + .with_lemmy_type(LemmyErrorType::InvalidRegex); if let Some(regex) = regex_str_opt { if regex.is_empty() { - None + match_nothing } 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) - } - }), - ) + 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 + match_nothing } } @@ -566,46 +569,39 @@ Line3", } #[test] - fn test_valid_slur_regex() { + fn test_valid_slur_regex() -> LemmyResult<()> { 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 - ); - } + build_and_check_regex(valid_regex)?; - #[test] - fn test_missing_slur_regex() { let missing_regex = None; - let result = build_and_check_regex(&missing_regex); - assert!(result.is_none()); - } + let match_none = build_and_check_regex(missing_regex)?; + assert!(!match_none.is_match("")); + assert!(!match_none.is_match("a")); - #[test] - fn test_empty_slur_regex() { let empty = Some(""); - let result = build_and_check_regex(&empty); - assert!(result.is_none()); + let match_none = build_and_check_regex(empty)?; + assert!(!match_none.is_match("")); + assert!(!match_none.is_match("a")); + + Ok(()) } #[test] fn test_too_permissive_slur_regex() { let match_everything_regexes = [ - (&Some("["), LemmyErrorType::InvalidRegex), - (&Some("(foo|bar|)"), LemmyErrorType::PermissiveRegex), - (&Some(".*"), LemmyErrorType::PermissiveRegex), + (Some("["), LemmyErrorType::InvalidRegex), + (Some("(foo|bar|)"), LemmyErrorType::PermissiveRegex), + (Some(".*"), LemmyErrorType::PermissiveRegex), ]; match_everything_regexes - .iter() + .into_iter() .for_each(|(regex_str, expected_err)| { let result = build_and_check_regex(regex_str); - assert!(result.as_ref().is_some_and(Result::is_err)); + assert!(result.is_err()); assert!( - result.is_some_and(|x| x.is_err_and(|e| e.error_type.eq(&expected_err.clone()))), + result.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 dbb09b078..825c31b56 160000 --- a/crates/utils/translations +++ b/crates/utils/translations @@ -1 +1 @@ -Subproject commit dbb09b0784982827d5d9b7dcf39f1703c1212b83 +Subproject commit 825c31b562318d4b2cef6eea777ef9625f12717f diff --git a/docker/Dockerfile b/docker/Dockerfile index 5bb39555a..062edb660 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,12 +38,6 @@ RUN --mount=type=cache,target=/lemmy/target set -ex; \ cargo clean --release; \ cargo build --features "${CARGO_BUILD_FEATURES}" --release; \ mv target/"${RUST_RELEASE_MODE}"/lemmy_server ./lemmy_server; \ - # - # Compress the binary with upx - wget https://github.com/upx/upx/releases/download/v4.2.4/upx-4.2.4-amd64_linux.tar.xz; \ - tar -xvf upx-4.2.4-amd64_linux.tar.xz; \ - cp upx-4.2.4-amd64_linux/upx /usr/bin; \ - upx --best --lzma lemmy_server; \ fi # ARM64 builder @@ -77,12 +71,6 @@ RUN --mount=type=cache,target=./target,uid=10001,gid=10001 set -ex; \ cargo clean --release; \ cargo build --features "${CARGO_BUILD_FEATURES}" --release; \ mv "./target/$CARGO_BUILD_TARGET/$RUST_RELEASE_MODE/lemmy_server" /home/lemmy/lemmy_server; \ - # - # Compress the binary with upx - wget https://github.com/upx/upx/releases/download/v4.2.4/upx-4.2.4-arm64_linux.tar.xz; \ - tar -xvf upx-4.2.4-arm64_linux.tar.xz; \ - cp upx-4.2.4-arm64_linux/upx /usr/bin; \ - upx --best --lzma /home/lemmy/lemmy_server; \ fi # amd64 base runner diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index dc978244e..42eb04898 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -38,7 +38,7 @@ services: hostname: lemmy restart: unless-stopped environment: - - RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug" + - RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug" - RUST_BACKTRACE=full ports: # prometheus metrics can be enabled with the `prometheus` config option. they are available on @@ -75,7 +75,7 @@ services: init: true pictrs: - image: asonix/pictrs:0.5.16 + image: asonix/pictrs:0.5.17-pre.9 # this needs to match the pictrs url in lemmy.hjson hostname: pictrs # we can set options to pictrs like this, here we set max. image size and forced format for conversion diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index bc4b5ea7f..7148216ad 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -16,7 +16,7 @@ x-lemmy-default: &lemmy-default dockerfile: docker/Dockerfile environment: - RUST_BACKTRACE=1 - - RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug" + - RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug" restart: always x-postgres-default: &postgres-default @@ -49,7 +49,7 @@ services: pictrs: restart: always - image: asonix/pictrs:0.5.16 + image: asonix/pictrs:0.5.17-pre.9 user: 991:991 volumes: - ./volumes/pictrs_alpha:/mnt:Z diff --git a/migrations/2024-11-25-161129_add_blurhash_to_image_details/down.sql b/migrations/2024-11-25-161129_add_blurhash_to_image_details/down.sql new file mode 100644 index 000000000..0f5a50e7d --- /dev/null +++ b/migrations/2024-11-25-161129_add_blurhash_to_image_details/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE image_details + DROP COLUMN blurhash; + diff --git a/migrations/2024-11-25-161129_add_blurhash_to_image_details/up.sql b/migrations/2024-11-25-161129_add_blurhash_to_image_details/up.sql new file mode 100644 index 000000000..14a6e253c --- /dev/null +++ b/migrations/2024-11-25-161129_add_blurhash_to_image_details/up.sql @@ -0,0 +1,5 @@ +-- Add a blurhash column for image_details +ALTER TABLE image_details +-- Supposed to be 20-30 chars, use 50 to be safe + ADD COLUMN blurhash varchar(50); + diff --git a/migrations/2024-12-12-222846_add_search_combined_table/down.sql b/migrations/2024-12-12-222846_add_search_combined_table/down.sql new file mode 100644 index 000000000..477bb9b63 --- /dev/null +++ b/migrations/2024-12-12-222846_add_search_combined_table/down.sql @@ -0,0 +1,5 @@ +ALTER TABLE person_aggregates + DROP COLUMN published; + +DROP TABLE search_combined; + diff --git a/migrations/2024-12-12-222846_add_search_combined_table/up.sql b/migrations/2024-12-12-222846_add_search_combined_table/up.sql new file mode 100644 index 000000000..f8edc7454 --- /dev/null +++ b/migrations/2024-12-12-222846_add_search_combined_table/up.sql @@ -0,0 +1,80 @@ +-- Creates combined tables for +-- Search: (post, comment, community, person) +CREATE TABLE search_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + -- This is used for the top sort + -- For persons: its post score + -- For comments: score, + -- For posts: score, + -- For community: users active monthly + score bigint NOT NULL DEFAULT 0, + post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, + community_id int UNIQUE REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE, + person_id int UNIQUE REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK (num_nonnulls (post_id, comment_id, community_id, person_id) = 1) +); + +CREATE INDEX idx_search_combined_published ON search_combined (published DESC, id DESC); + +CREATE INDEX idx_search_combined_published_asc ON search_combined (reverse_timestamp_sort (published) DESC, id DESC); + +CREATE INDEX idx_search_combined_score ON search_combined (score DESC, id DESC); + +-- Add published to person_aggregates (it was missing for some reason) +ALTER TABLE person_aggregates + ADD COLUMN published timestamptz NOT NULL DEFAULT now(); + +UPDATE + person_aggregates pa +SET + published = p.published +FROM + person p +WHERE + pa.person_id = p.id; + +-- Updating the history +INSERT INTO search_combined (published, score, post_id, comment_id, community_id, person_id) +SELECT + published, + score, + post_id, + NULL::int, + NULL::int, + NULL::int +FROM + post_aggregates +UNION ALL +SELECT + published, + score, + NULL::int, + comment_id, + NULL::int, + NULL::int +FROM + comment_aggregates +UNION ALL +SELECT + published, + users_active_month, + NULL::int, + NULL::int, + community_id, + NULL::int +FROM + community_aggregates +UNION ALL +SELECT + published, + post_score, + NULL::int, + NULL::int, + NULL::int, + person_id +FROM + person_aggregates; + diff --git a/migrations/2025-01-21-000000_interactions_per_month_schema/down.sql b/migrations/2025-01-21-000000_interactions_per_month_schema/down.sql new file mode 100644 index 000000000..22f2af14e --- /dev/null +++ b/migrations/2025-01-21-000000_interactions_per_month_schema/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE community_aggregates + DROP COLUMN interactions_month; + diff --git a/migrations/2025-01-21-000000_interactions_per_month_schema/up.sql b/migrations/2025-01-21-000000_interactions_per_month_schema/up.sql new file mode 100644 index 000000000..2ad5bf688 --- /dev/null +++ b/migrations/2025-01-21-000000_interactions_per_month_schema/up.sql @@ -0,0 +1,4 @@ +-- Add the interactions_month column +ALTER TABLE community_aggregates + ADD COLUMN interactions_month bigint NOT NULL DEFAULT 0; + diff --git a/migrations/2025-01-23-143621_report_to_admins/down.sql b/migrations/2025-01-23-143621_report_to_admins/down.sql new file mode 100644 index 000000000..779d47d31 --- /dev/null +++ b/migrations/2025-01-23-143621_report_to_admins/down.sql @@ -0,0 +1,6 @@ +ALTER TABLE post_report + DROP COLUMN violates_instance_rules; + +ALTER TABLE comment_report + DROP COLUMN violates_instance_rules; + diff --git a/migrations/2025-01-23-143621_report_to_admins/up.sql b/migrations/2025-01-23-143621_report_to_admins/up.sql new file mode 100644 index 000000000..ecf7e5d29 --- /dev/null +++ b/migrations/2025-01-23-143621_report_to_admins/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE post_report + ADD COLUMN violates_instance_rules bool NOT NULL DEFAULT FALSE; + +ALTER TABLE comment_report + ADD COLUMN violates_instance_rules bool NOT NULL DEFAULT FALSE; + diff --git a/migrations/2025-02-05-090155_ap_id/down.sql b/migrations/2025-02-05-090155_ap_id/down.sql new file mode 100644 index 000000000..f2dc19a4e --- /dev/null +++ b/migrations/2025-02-05-090155_ap_id/down.sql @@ -0,0 +1,6 @@ +ALTER TABLE person RENAME ap_id TO actor_id; + +ALTER TABLE community RENAME ap_id TO actor_id; + +ALTER TABLE site RENAME ap_id TO actor_id; + diff --git a/migrations/2025-02-05-090155_ap_id/up.sql b/migrations/2025-02-05-090155_ap_id/up.sql new file mode 100644 index 000000000..ec7f9c74c --- /dev/null +++ b/migrations/2025-02-05-090155_ap_id/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE person RENAME actor_id TO ap_id; + +ALTER TABLE community RENAME actor_id TO ap_id; + +ALTER TABLE site RENAME actor_id TO ap_id; + diff --git a/migrations/2025-02-06-233105_remove_post_sort_type_enums/down.sql b/migrations/2025-02-06-233105_remove_post_sort_type_enums/down.sql new file mode 100644 index 000000000..b8e5d45e6 --- /dev/null +++ b/migrations/2025-02-06-233105_remove_post_sort_type_enums/down.sql @@ -0,0 +1,76 @@ +-- This removes all the extra post_sort_type_enums, +-- and adds a default_post_time_range_seconds field. +-- Drop the defaults because of a postgres bug +ALTER TABLE local_user + ALTER default_post_sort_type DROP DEFAULT; + +ALTER TABLE local_site + ALTER default_post_sort_type DROP DEFAULT; + +-- Change all the top variants to top in the two tables that use the enum +UPDATE + local_user +SET + default_post_sort_type = 'Active' +WHERE + default_post_sort_type = 'Top'; + +UPDATE + local_site +SET + default_post_sort_type = 'Active' +WHERE + default_post_sort_type = 'Top'; + +-- rename the old enum to a tmp name +ALTER TYPE post_sort_type_enum RENAME TO post_sort_type_enum__; + +-- create the new enum +CREATE TYPE post_sort_type_enum AS ENUM ( + 'Active', + 'Hot', + 'New', + 'Old', + 'TopDay', + 'TopWeek', + 'TopMonth', + 'TopYear', + 'TopAll', + 'MostComments', + 'NewComments', + 'TopHour', + 'TopSixHour', + 'TopTwelveHour', + 'TopThreeMonths', + 'TopSixMonths', + 'TopNineMonths', + 'Controversial', + 'Scaled' +); + +-- alter all you enum columns +ALTER TABLE local_user + ALTER COLUMN default_post_sort_type TYPE post_sort_type_enum + USING default_post_sort_type::text::post_sort_type_enum; + +ALTER TABLE local_site + ALTER COLUMN default_post_sort_type TYPE post_sort_type_enum + USING default_post_sort_type::text::post_sort_type_enum; + +-- drop the old enum +DROP TYPE post_sort_type_enum__; + +-- Add back in the default +ALTER TABLE local_user + ALTER default_post_sort_type SET DEFAULT 'Active'; + +ALTER TABLE local_site + ALTER default_post_sort_type SET DEFAULT 'Active'; + +-- Drop the new columns +ALTER TABLE local_user + DROP COLUMN default_post_time_range_seconds; + +ALTER TABLE local_site + DROP COLUMN default_post_time_range_seconds; + diff --git a/migrations/2025-02-06-233105_remove_post_sort_type_enums/up.sql b/migrations/2025-02-06-233105_remove_post_sort_type_enums/up.sql new file mode 100644 index 000000000..0d46e5a64 --- /dev/null +++ b/migrations/2025-02-06-233105_remove_post_sort_type_enums/up.sql @@ -0,0 +1,69 @@ +-- This removes all the extra post_sort_type_enums, +-- and adds a default_post_time_range_seconds field. +-- Change all the top variants to top in the two tables that use the enum +-- Because of a postgres bug, you can't assign this to a new enum value, +-- unless you run an unsafe commit first. So just use active. +-- https://dba.stackexchange.com/questions/280371/postgres-unsafe-use-of-new-value-of-enum-type +UPDATE + local_user +SET + default_post_sort_type = 'Active' +WHERE + default_post_sort_type IN ('TopDay', 'TopWeek', 'TopMonth', 'TopYear', 'TopAll', 'TopHour', 'TopSixHour', 'TopTwelveHour', 'TopThreeMonths', 'TopSixMonths', 'TopNineMonths'); + +UPDATE + local_site +SET + default_post_sort_type = 'Active' +WHERE + default_post_sort_type IN ('TopDay', 'TopWeek', 'TopMonth', 'TopYear', 'TopAll', 'TopHour', 'TopSixHour', 'TopTwelveHour', 'TopThreeMonths', 'TopSixMonths', 'TopNineMonths'); + +-- Drop the defaults because of a postgres bug +ALTER TABLE local_user + ALTER default_post_sort_type DROP DEFAULT; + +ALTER TABLE local_site + ALTER default_post_sort_type DROP DEFAULT; + +-- rename the old enum to a tmp name +ALTER TYPE post_sort_type_enum RENAME TO post_sort_type_enum__; + +-- create the new enum +CREATE TYPE post_sort_type_enum AS ENUM ( + 'Active', + 'Hot', + 'New', + 'Old', + 'Top', + 'MostComments', + 'NewComments', + 'Controversial', + 'Scaled' +); + +-- alter all you enum columns +ALTER TABLE local_user + ALTER COLUMN default_post_sort_type TYPE post_sort_type_enum + USING default_post_sort_type::text::post_sort_type_enum; + +ALTER TABLE local_site + ALTER COLUMN default_post_sort_type TYPE post_sort_type_enum + USING default_post_sort_type::text::post_sort_type_enum; + +-- drop the old enum +DROP TYPE post_sort_type_enum__; + +-- Add back in the default +ALTER TABLE local_user + ALTER default_post_sort_type SET DEFAULT 'Active'; + +ALTER TABLE local_site + ALTER default_post_sort_type SET DEFAULT 'Active'; + +-- Add the new column to both tables (null means no limit) +ALTER TABLE local_user + ADD COLUMN default_post_time_range_seconds INTEGER; + +ALTER TABLE local_site + ADD COLUMN default_post_time_range_seconds INTEGER; + diff --git a/migrations/2025-02-11-131045_ban-remove-content-pm/down.sql b/migrations/2025-02-11-131045_ban-remove-content-pm/down.sql new file mode 100644 index 000000000..b535bb021 --- /dev/null +++ b/migrations/2025-02-11-131045_ban-remove-content-pm/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE private_message + DROP COLUMN removed; + diff --git a/migrations/2025-02-11-131045_ban-remove-content-pm/up.sql b/migrations/2025-02-11-131045_ban-remove-content-pm/up.sql new file mode 100644 index 000000000..9724bee16 --- /dev/null +++ b/migrations/2025-02-11-131045_ban-remove-content-pm/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE private_message + ADD COLUMN removed bool NOT NULL DEFAULT FALSE; + diff --git a/migrations/2025-02-18-143408_block_nsfw/down.sql b/migrations/2025-02-18-143408_block_nsfw/down.sql new file mode 100644 index 000000000..03b82d6f7 --- /dev/null +++ b/migrations/2025-02-18-143408_block_nsfw/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_site + DROP COLUMN disallow_nsfw_content; + diff --git a/migrations/2025-02-18-143408_block_nsfw/up.sql b/migrations/2025-02-18-143408_block_nsfw/up.sql new file mode 100644 index 000000000..b7e7fc4b8 --- /dev/null +++ b/migrations/2025-02-18-143408_block_nsfw/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_site + ADD COLUMN disallow_nsfw_content boolean DEFAULT FALSE NOT NULL; + diff --git a/migrations/2025-02-24-173152_search-alt-text-of-posts/down.sql b/migrations/2025-02-24-173152_search-alt-text-of-posts/down.sql new file mode 100644 index 000000000..5d1b72611 --- /dev/null +++ b/migrations/2025-02-24-173152_search-alt-text-of-posts/down.sql @@ -0,0 +1,4 @@ +DROP INDEX idx_post_trigram; + +CREATE INDEX IF NOT EXISTS idx_post_trigram ON post USING gin (name gin_trgm_ops, body gin_trgm_ops); + diff --git a/migrations/2025-02-24-173152_search-alt-text-of-posts/up.sql b/migrations/2025-02-24-173152_search-alt-text-of-posts/up.sql new file mode 100644 index 000000000..21eb20633 --- /dev/null +++ b/migrations/2025-02-24-173152_search-alt-text-of-posts/up.sql @@ -0,0 +1,4 @@ +DROP INDEX idx_post_trigram; + +CREATE INDEX IF NOT EXISTS idx_post_trigram ON post USING gin (name gin_trgm_ops, body gin_trgm_ops, alt_text gin_trgm_ops); + diff --git a/migrations/2025-03-04-105516_remove-aggregate-tables/down.sql b/migrations/2025-03-04-105516_remove-aggregate-tables/down.sql new file mode 100644 index 000000000..6dfb0d905 --- /dev/null +++ b/migrations/2025-03-04-105516_remove-aggregate-tables/down.sql @@ -0,0 +1,351 @@ +-- move comment_aggregates back into separate table +CREATE TABLE comment_aggregates ( + comment_id int PRIMARY KEY NOT NULL REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + score bigint NOT NULL DEFAULT 0, + upvotes bigint NOT NULL DEFAULT 0, + downvotes bigint NOT NULL DEFAULT 0, + published timestamp with time zone NOT NULL DEFAULT now(), + child_count integer NOT NULL DEFAULT 0, + hot_rank double precision NOT NULL DEFAULT 0.0001, + controversy_rank double precision NOT NULL DEFAULT 0, + report_count smallint NOT NULL DEFAULT 0, + unresolved_report_count smallint NOT NULL DEFAULT 0 +); + +INSERT INTO comment_aggregates +SELECT + id AS comment_id, + score, + upvotes, + downvotes, + published, + child_count, + hot_rank, + controversy_rank, + report_count, + unresolved_report_count +FROM + comment; + +ALTER TABLE comment + DROP COLUMN score, + DROP COLUMN upvotes, + DROP COLUMN downvotes, + DROP COLUMN child_count, + DROP COLUMN hot_rank, + DROP COLUMN controversy_rank, + DROP COLUMN report_count, + DROP COLUMN unresolved_report_count; + +CREATE INDEX idx_comment_aggregates_controversy ON comment_aggregates USING btree (controversy_rank DESC); + +CREATE INDEX idx_comment_aggregates_hot ON comment_aggregates USING btree (hot_rank DESC, score DESC); + +CREATE INDEX idx_comment_aggregates_nonzero_hotrank ON comment_aggregates USING btree (published) +WHERE (hot_rank <> (0)::double precision); + +CREATE INDEX idx_comment_aggregates_published ON comment_aggregates USING btree (published DESC); + +CREATE INDEX idx_comment_aggregates_score ON comment_aggregates USING btree (score DESC); + +-- move comment_aggregates back into separate table +CREATE TABLE post_aggregates ( + post_id int PRIMARY KEY NOT NULL REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + comments bigint NOT NULL DEFAULT 0, + score bigint NOT NULL DEFAULT 0, + upvotes bigint NOT NULL DEFAULT 0, + downvotes bigint NOT NULL DEFAULT 0, + published timestamp with time zone NOT NULL DEFAULT now(), + newest_comment_time_necro timestamp with time zone NOT NULL DEFAULT now(), + newest_comment_time timestamp with time zone NOT NULL DEFAULT now(), + featured_community boolean NOT NULL DEFAULT FALSE, + featured_local boolean NOT NULL DEFAULT FALSE, + hot_rank double precision NOT NULL DEFAULT 0.0001, + hot_rank_active double precision NOT NULL DEFAULT 0.0001, + community_id integer NOT NULL REFERENCES community (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + creator_id integer NOT NULL REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + controversy_rank double precision NOT NULL DEFAULT 0, + instance_id integer NOT NULL REFERENCES instance (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + scaled_rank double precision NOT NULL DEFAULT 0.0001, + report_count smallint NOT NULL DEFAULT 0, + unresolved_report_count smallint NOT NULL DEFAULT 0 +); + +INSERT INTO post_aggregates +SELECT + id AS post_id, + comments, + score, + upvotes, + downvotes, + published, + newest_comment_time_necro, + newest_comment_time, + featured_community, + featured_local, + hot_rank, + hot_rank_active, + community_id, + creator_id, + controversy_rank, + instance_id, + scaled_rank, + report_count, + unresolved_report_count +FROM + post; + +ALTER TABLE post + DROP COLUMN comments, + DROP COLUMN score, + DROP COLUMN upvotes, + DROP COLUMN downvotes, + DROP COLUMN newest_comment_time_necro, + DROP COLUMN newest_comment_time, + DROP COLUMN hot_rank, + DROP COLUMN hot_rank_active, + DROP COLUMN controversy_rank, + DROP COLUMN instance_id, + DROP COLUMN scaled_rank, + DROP COLUMN report_count, + DROP COLUMN unresolved_report_count; + +CREATE INDEX idx_post_aggregates_community_active ON post_aggregates USING btree (community_id, featured_local DESC, hot_rank_active DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_controversy ON post_aggregates USING btree (community_id, featured_local DESC, controversy_rank DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_hot ON post_aggregates USING btree (community_id, featured_local DESC, hot_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_most_comments ON post_aggregates USING btree (community_id, featured_local DESC, comments DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_newest_comment_time ON post_aggregates USING btree (community_id, featured_local DESC, newest_comment_time DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_newest_comment_time_necro ON post_aggregates USING btree (community_id, featured_local DESC, newest_comment_time_necro DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_published ON post_aggregates USING btree (community_id, featured_local DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_published_asc ON post_aggregates USING btree (community_id, featured_local DESC, reverse_timestamp_sort (published) DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_scaled ON post_aggregates USING btree (community_id, featured_local DESC, scaled_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_community_score ON post_aggregates USING btree (community_id, featured_local DESC, score DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_active ON post_aggregates USING btree (community_id, featured_community DESC, hot_rank_active DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_controversy ON post_aggregates USING btree (community_id, featured_community DESC, controversy_rank DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_hot ON post_aggregates USING btree (community_id, featured_community DESC, hot_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_most_comments ON post_aggregates USING btree (community_id, featured_community DESC, comments DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_newest_comment_time ON post_aggregates USING btree (community_id, featured_community DESC, newest_comment_time DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_newest_comment_time_necr ON post_aggregates USING btree (community_id, featured_community DESC, newest_comment_time_necro DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_published ON post_aggregates USING btree (community_id, featured_community DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_published_asc ON post_aggregates USING btree (community_id, featured_community DESC, reverse_timestamp_sort (published) DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_scaled ON post_aggregates USING btree (community_id, featured_community DESC, scaled_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_community_score ON post_aggregates USING btree (community_id, featured_community DESC, score DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_active ON post_aggregates USING btree (featured_local DESC, hot_rank_active DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_controversy ON post_aggregates USING btree (featured_local DESC, controversy_rank DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_hot ON post_aggregates USING btree (featured_local DESC, hot_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_most_comments ON post_aggregates USING btree (featured_local DESC, comments DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_newest_comment_time ON post_aggregates USING btree (featured_local DESC, newest_comment_time DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_newest_comment_time_necro ON post_aggregates USING btree (featured_local DESC, newest_comment_time_necro DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_published ON post_aggregates USING btree (featured_local DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_published_asc ON post_aggregates USING btree (featured_local DESC, reverse_timestamp_sort (published) DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_scaled ON post_aggregates USING btree (featured_local DESC, scaled_rank DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_featured_local_score ON post_aggregates USING btree (featured_local DESC, score DESC, published DESC, post_id DESC); + +CREATE INDEX idx_post_aggregates_nonzero_hotrank ON post_aggregates USING btree (published DESC) +WHERE ((hot_rank <> (0)::double precision) OR (hot_rank_active <> (0)::double precision)); + +CREATE INDEX idx_post_aggregates_published ON post_aggregates USING btree (published DESC); + +CREATE INDEX idx_post_aggregates_published_asc ON post_aggregates USING btree (reverse_timestamp_sort (published) DESC); + +DROP INDEX idx_post_featured_community_published_asc; + +DROP INDEX idx_post_featured_local_published; + +DROP INDEX idx_post_featured_local_published_asc; + +DROP INDEX idx_post_published; + +DROP INDEX idx_post_published_asc; + +DROP INDEX idx_search_combined_score; + +-- move community_aggregates back into separate table +CREATE TABLE community_aggregates ( + community_id int PRIMARY KEY NOT NULL REFERENCES COMMunity ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + subscribers bigint NOT NULL DEFAULT 0, + posts bigint NOT NULL DEFAULT 0, + comments bigint NOT NULL DEFAULT 0, + published timestamp with time zone DEFAULT now() NOT NULL, + users_active_day bigint NOT NULL DEFAULT 0, + users_active_week bigint NOT NULL DEFAULT 0, + users_active_month bigint NOT NULL DEFAULT 0, + users_active_half_year bigint NOT NULL DEFAULT 0, + hot_rank double precision NOT NULL DEFAULT 0.0001, + subscribers_local bigint NOT NULL DEFAULT 0, + report_count smallint NOT NULL DEFAULT 0, + unresolved_report_count smallint NOT NULL DEFAULT 0, + interactions_month bigint NOT NULL DEFAULT 0 +); + +INSERT INTO community_aggregates +SELECT + id AS comment_id, + subscribers, + posts, + comments, + published, + users_active_day, + users_active_week, + users_active_month, + users_active_half_year, + hot_rank, + subscribers_local, + report_count, + unresolved_report_count, + interactions_month +FROM + community; + +ALTER TABLE community + DROP COLUMN subscribers, + DROP COLUMN posts, + DROP COLUMN comments, + DROP COLUMN users_active_day, + DROP COLUMN users_active_week, + DROP COLUMN users_active_month, + DROP COLUMN users_active_half_year, + DROP COLUMN hot_rank, + DROP COLUMN subscribers_local, + DROP COLUMN report_count, + DROP COLUMN unresolved_report_count, + DROP COLUMN interactions_month, + ALTER CONSTRAINT community_instance_id_fkey NOT DEFERRABLE INITIALLY IMMEDIATE; + +CREATE INDEX idx_community_aggregates_hot ON public.community_aggregates USING btree (hot_rank DESC); + +CREATE INDEX idx_community_aggregates_nonzero_hotrank ON public.community_aggregates USING btree (published) +WHERE (hot_rank <> (0)::double precision); + +CREATE INDEX idx_community_aggregates_published ON public.community_aggregates USING btree (published DESC); + +CREATE INDEX idx_community_aggregates_subscribers ON public.community_aggregates USING btree (subscribers DESC); + +CREATE INDEX idx_community_aggregates_users_active_month ON public.community_aggregates USING btree (users_active_month DESC); + +-- move person_aggregates back into separate table +CREATE TABLE person_aggregates ( + person_id int PRIMARY KEY NOT NULL REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + post_count bigint NOT NULL DEFAULT 0, + post_score bigint NOT NULL DEFAULT 0, + comment_count bigint NOT NULL DEFAULT 0, + comment_score bigint NOT NULL DEFAULT 0, + published timestamp with time zone DEFAULT now() NOT NULL +); + +INSERT INTO person_aggregates +SELECT + id AS person_id, + post_count, + post_score, + comment_count, + comment_score, + published +FROM + person; + +ALTER TABLE person + DROP COLUMN post_count, + DROP COLUMN post_score, + DROP COLUMN comment_count, + DROP COLUMN comment_score; + +CREATE INDEX idx_person_aggregates_comment_score ON public.person_aggregates USING btree (comment_score DESC); + +CREATE INDEX idx_person_aggregates_person ON public.person_aggregates USING btree (person_id); + +-- move site_aggregates back into separate table +CREATE TABLE site_aggregates ( + site_id int PRIMARY KEY NOT NULL REFERENCES site ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + users bigint NOT NULL DEFAULT 1, + posts bigint NOT NULL DEFAULT 0, + comments bigint NOT NULL DEFAULT 0, + communities bigint NOT NULL DEFAULT 0, + users_active_day bigint NOT NULL DEFAULT 0, + users_active_week bigint NOT NULL DEFAULT 0, + users_active_month bigint NOT NULL DEFAULT 0, + users_active_half_year bigint NOT NULL DEFAULT 0 +); + +INSERT INTO site_aggregates +SELECT + id AS site_id, + users, + posts, + comments, + communities, + users_active_day, + users_active_week, + users_active_month, + users_active_half_year +FROM + local_site; + +ALTER TABLE local_site + DROP COLUMN users, + DROP COLUMN posts, + DROP COLUMN comments, + DROP COLUMN communities, + DROP COLUMN users_active_day, + DROP COLUMN users_active_week, + DROP COLUMN users_active_month, + DROP COLUMN users_active_half_year; + +-- move local_user_vote_display_mode back into separate table +CREATE TABLE local_user_vote_display_mode ( + local_user_id int PRIMARY KEY NOT NULL REFERENCES local_user ON UPDATE CASCADE ON DELETE CASCADE, + score boolean NOT NULL DEFAULT FALSE, + upvotes boolean NOT NULL DEFAULT TRUE, + downvotes boolean NOT NULL DEFAULT TRUE, + upvote_percentage boolean NOT NULL DEFAULT FALSE +); + +INSERT INTO local_user_vote_display_mode +SELECT + id AS local_user_id, + show_score AS score, + show_upvotes AS upvotes, + show_downvotes AS downvotes, + show_upvote_percentage AS upvote_percentage +FROM + local_user; + +ALTER TABLE local_user + DROP COLUMN show_score, + DROP COLUMN show_upvotes, + DROP COLUMN show_downvotes, + DROP COLUMN show_upvote_percentage; + +CREATE INDEX idx_search_combined_score ON public.search_combined USING btree (score DESC, id DESC); + +CREATE UNIQUE INDEX idx_site_aggregates_1_row_only ON public.site_aggregates USING btree ((TRUE)); + diff --git a/migrations/2025-03-04-105516_remove-aggregate-tables/up.sql b/migrations/2025-03-04-105516_remove-aggregate-tables/up.sql new file mode 100644 index 000000000..001c9e06d --- /dev/null +++ b/migrations/2025-03-04-105516_remove-aggregate-tables/up.sql @@ -0,0 +1,260 @@ +-- merge comment_aggregates into comment table +ALTER TABLE comment + ADD COLUMN score bigint NOT NULL DEFAULT 0, + ADD COLUMN upvotes bigint NOT NULL DEFAULT 0, + ADD COLUMN downvotes bigint NOT NULL DEFAULT 0, + ADD COLUMN child_count integer NOT NULL DEFAULT 0, + ADD COLUMN hot_rank double precision NOT NULL DEFAULT 0.0001, + ADD COLUMN controversy_rank double precision NOT NULL DEFAULT 0, + ADD COLUMN report_count smallint NOT NULL DEFAULT 0, + ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0; + +UPDATE + comment +SET + score = ca.score, + upvotes = ca.upvotes, + downvotes = ca.downvotes, + child_count = ca.child_count, + hot_rank = ca.hot_rank, + controversy_rank = ca.controversy_rank, + report_count = ca.report_count, + unresolved_report_count = ca.unresolved_report_count +FROM + comment_aggregates AS ca +WHERE + comment.id = ca.comment_id; + +DROP TABLE comment_aggregates; + +CREATE INDEX idx_comment_controversy ON comment USING btree (controversy_rank DESC); + +CREATE INDEX idx_comment_hot ON comment USING btree (hot_rank DESC, score DESC); + +CREATE INDEX idx_comment_nonzero_hotrank ON comment USING btree (published) +WHERE (hot_rank <> (0)::double precision); + +--CREATE INDEX idx_comment_published on comment USING btree (published DESC); +CREATE INDEX idx_comment_score ON comment USING btree (score DESC); + +-- merge post_aggregates into post table +ALTER TABLE post + ADD COLUMN comments bigint NOT NULL DEFAULT 0, + ADD COLUMN score bigint NOT NULL DEFAULT 0, + ADD COLUMN upvotes bigint NOT NULL DEFAULT 0, + ADD COLUMN downvotes bigint NOT NULL DEFAULT 0, + ADD COLUMN newest_comment_time_necro timestamp with time zone NOT NULL DEFAULT now(), + ADD COLUMN newest_comment_time timestamp with time zone NOT NULL DEFAULT now(), + ADD COLUMN hot_rank double precision NOT NULL DEFAULT 0.0001, + ADD COLUMN hot_rank_active double precision NOT NULL DEFAULT 0.0001, + ADD COLUMN controversy_rank double precision NOT NULL DEFAULT 0, + ADD COLUMN instance_id int NOT NULL DEFAULT 0 REFERENCES instance (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + ADD COLUMN scaled_rank double precision NOT NULL DEFAULT 0.0001, + ADD COLUMN report_count smallint NOT NULL DEFAULT 0, + ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0; + +UPDATE + post +SET + comments = pa.comments, + score = pa.score, + upvotes = pa.upvotes, + downvotes = pa.downvotes, + newest_comment_time_necro = pa.newest_comment_time_necro, + newest_comment_time = pa.newest_comment_time, + hot_rank = pa.hot_rank, + hot_rank_active = pa.hot_rank_active, + controversy_rank = pa.controversy_rank, + instance_id = pa.instance_id, + scaled_rank = pa.scaled_rank, + report_count = pa.report_count, + unresolved_report_count = pa.unresolved_report_count +FROM + post_aggregates AS pa +WHERE + post.id = pa.post_id; + +DROP TABLE post_aggregates; + +CREATE INDEX idx_post_community_active ON post USING btree (community_id, featured_local DESC, hot_rank_active DESC, published DESC, id DESC); + +CREATE INDEX idx_post_community_controversy ON post USING btree (community_id, featured_local DESC, controversy_rank DESC, id DESC); + +CREATE INDEX idx_post_community_hot ON post USING btree (community_id, featured_local DESC, hot_rank DESC, published DESC, id DESC); + +CREATE INDEX idx_post_community_most_comments ON post USING btree (community_id, featured_local DESC, comments DESC, published DESC, id DESC); + +CREATE INDEX idx_post_community_newest_comment_time ON post USING btree (community_id, featured_local DESC, newest_comment_time DESC, id DESC); + +CREATE INDEX idx_post_community_newest_comment_time_necro ON post USING btree (community_id, featured_local DESC, newest_comment_time_necro DESC, id DESC); + +-- INDEX idx_post_community_published ON post USING btree (community_id, featured_local DESC, published DESC); +--CREATE INDEX idx_post_community_published_asc ON post USING btree (community_id, featured_local DESC, reverse_timestamp_sort (published) DESC); +CREATE INDEX idx_post_community_scaled ON post USING btree (community_id, featured_local DESC, scaled_rank DESC, published DESC, id DESC); + +CREATE INDEX idx_post_community_score ON post USING btree (community_id, featured_local DESC, score DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_community_active ON post USING btree (community_id, featured_community DESC, hot_rank_active DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_community_controversy ON post USING btree (community_id, featured_community DESC, controversy_rank DESC, id DESC); + +CREATE INDEX idx_post_featured_community_hot ON post USING btree (community_id, featured_community DESC, hot_rank DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_community_most_comments ON post USING btree (community_id, featured_community DESC, comments DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_community_newest_comment_time ON post USING btree (community_id, featured_community DESC, newest_comment_time DESC, id DESC); + +CREATE INDEX idx_post_featured_community_newest_comment_time_necr ON post USING btree (community_id, featured_community DESC, newest_comment_time_necro DESC, id DESC); + +--CREATE INDEX idx_post_featured_community_published ON post USING btree (community_id, featured_community DESC, published DESC); +CREATE INDEX idx_post_featured_community_published_asc ON post USING btree (community_id, featured_community DESC, reverse_timestamp_sort (published) DESC, id DESC); + +CREATE INDEX idx_post_featured_community_scaled ON post USING btree (community_id, featured_community DESC, scaled_rank DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_community_score ON post USING btree (community_id, featured_community DESC, score DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_local_active ON post USING btree (featured_local DESC, hot_rank_active DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_local_controversy ON post USING btree (featured_local DESC, controversy_rank DESC, id DESC); + +CREATE INDEX idx_post_featured_local_hot ON post USING btree (featured_local DESC, hot_rank DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_local_most_comments ON post USING btree (featured_local DESC, comments DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_local_newest_comment_time ON post USING btree (featured_local DESC, newest_comment_time DESC, id DESC); + +CREATE INDEX idx_post_featured_local_newest_comment_time_necro ON post USING btree (featured_local DESC, newest_comment_time_necro DESC, id DESC); + +CREATE INDEX idx_post_featured_local_published ON post USING btree (featured_local DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_local_published_asc ON post USING btree (featured_local DESC, reverse_timestamp_sort (published) DESC, id DESC); + +CREATE INDEX idx_post_featured_local_scaled ON post USING btree (featured_local DESC, scaled_rank DESC, published DESC, id DESC); + +CREATE INDEX idx_post_featured_local_score ON post USING btree (featured_local DESC, score DESC, published DESC, id DESC); + +CREATE INDEX idx_post_nonzero_hotrank ON post USING btree (published DESC) +WHERE ((hot_rank <> (0)::double precision) OR (hot_rank_active <> (0)::double precision)); + +CREATE INDEX idx_post_published ON post USING btree (published DESC); + +CREATE INDEX idx_post_published_asc ON post USING btree (reverse_timestamp_sort (published) DESC); + +-- merge community_aggregates into community table +ALTER TABLE community + ADD COLUMN subscribers bigint NOT NULL DEFAULT 0, + ADD COLUMN posts bigint NOT NULL DEFAULT 0, + ADD COLUMN comments bigint NOT NULL DEFAULT 0, + ADD COLUMN users_active_day bigint NOT NULL DEFAULT 0, + ADD COLUMN users_active_week bigint NOT NULL DEFAULT 0, + ADD COLUMN users_active_month bigint NOT NULL DEFAULT 0, + ADD COLUMN users_active_half_year bigint NOT NULL DEFAULT 0, + ADD COLUMN hot_rank double precision NOT NULL DEFAULT 0.0001, + ADD COLUMN subscribers_local bigint NOT NULL DEFAULT 0, + ADD COLUMN report_count smallint NOT NULL DEFAULT 0, + ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0, + ADD COLUMN interactions_month bigint NOT NULL DEFAULT 0, + ALTER CONSTRAINT community_instance_id_fkey DEFERRABLE INITIALLY DEFERRED; + +UPDATE + community +SET + subscribers = ca.subscribers, + posts = ca.posts, + comments = ca.comments, + users_active_day = ca.users_active_day, + users_active_week = ca.users_active_week, + users_active_month = ca.users_active_month, + users_active_half_year = ca.users_active_half_year, + hot_rank = ca.hot_rank, + subscribers_local = ca.subscribers_local, + report_count = ca.report_count, + unresolved_report_count = ca.unresolved_report_count, + interactions_month = ca.interactions_month +FROM + community_aggregates AS ca +WHERE + community.id = ca.community_id; + +DROP TABLE community_aggregates; + +CREATE INDEX idx_community_hot ON public.community USING btree (hot_rank DESC); + +CREATE INDEX idx_community_nonzero_hotrank ON public.community USING btree (published) +WHERE (hot_rank <> (0)::double precision); + +CREATE INDEX idx_community_subscribers ON public.community USING btree (subscribers DESC); + +CREATE INDEX idx_community_users_active_month ON public.community USING btree (users_active_month DESC); + +-- merge person_aggregates into person table +ALTER TABLE person + ADD COLUMN post_count bigint NOT NULL DEFAULT 0, + ADD COLUMN post_score bigint NOT NULL DEFAULT 0, + ADD COLUMN comment_count bigint NOT NULL DEFAULT 0, + ADD COLUMN comment_score bigint NOT NULL DEFAULT 0; + +UPDATE + person +SET + post_count = pa.post_count, + post_score = pa.post_score, + comment_count = pa.comment_count, + comment_score = pa.comment_score +FROM + person_aggregates AS pa +WHERE + person.id = pa.person_id; + +DROP TABLE person_aggregates; + +-- merge site_aggregates into person table +ALTER TABLE local_site + ADD COLUMN users bigint NOT NULL DEFAULT 1, + ADD COLUMN posts bigint NOT NULL DEFAULT 0, + ADD COLUMN comments bigint NOT NULL DEFAULT 0, + ADD COLUMN communities bigint NOT NULL DEFAULT 0, + ADD COLUMN users_active_day bigint NOT NULL DEFAULT 0, + ADD COLUMN users_active_week bigint NOT NULL DEFAULT 0, + ADD COLUMN users_active_month bigint NOT NULL DEFAULT 0, + ADD COLUMN users_active_half_year bigint NOT NULL DEFAULT 0; + +UPDATE + local_site +SET + users = sa.users, + posts = sa.posts, + comments = sa.comments, + communities = sa.communities, + users_active_day = sa.users_active_day, + users_active_week = sa.users_active_week, + users_active_month = sa.users_active_month, + users_active_half_year = sa.users_active_half_year +FROM + site_aggregates AS sa +WHERE + local_site.site_id = sa.site_id; + +DROP TABLE site_aggregates; + +-- merge local_user_vote_display_mode into local_user table +ALTER TABLE local_user + ADD COLUMN show_score boolean NOT NULL DEFAULT FALSE, + ADD COLUMN show_upvotes boolean NOT NULL DEFAULT TRUE, + ADD COLUMN show_downvotes boolean NOT NULL DEFAULT TRUE, + ADD COLUMN show_upvote_percentage boolean NOT NULL DEFAULT FALSE; + +UPDATE + local_user +SET + show_score = v.score, + show_upvotes = v.upvotes, + show_downvotes = v.downvotes, + show_upvote_percentage = v.upvote_percentage +FROM + local_user_vote_display_mode AS v +WHERE + local_user.id = v.local_user_id; + +DROP TABLE local_user_vote_display_mode; + diff --git a/migrations/2025-03-07-094522_enable_english_for_all/down.sql b/migrations/2025-03-07-094522_enable_english_for_all/down.sql new file mode 100644 index 000000000..deb75def2 --- /dev/null +++ b/migrations/2025-03-07-094522_enable_english_for_all/down.sql @@ -0,0 +1,3 @@ +SELECT + 1; + diff --git a/migrations/2025-03-07-094522_enable_english_for_all/up.sql b/migrations/2025-03-07-094522_enable_english_for_all/up.sql new file mode 100644 index 000000000..0fc642276 --- /dev/null +++ b/migrations/2025-03-07-094522_enable_english_for_all/up.sql @@ -0,0 +1,12 @@ +-- enable english for all users +INSERT INTO local_user_language (local_user_id, language_id) +SELECT + local_user_id, + 37 +FROM + local_user_language +GROUP BY + local_user_id +HAVING + NOT (37 = ANY (array_agg(language_id))); + diff --git a/scripts/release.sh b/scripts/release.sh index 79209f3b4..9bc1c91f5 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -26,15 +26,15 @@ fi old_tag=$(grep version Cargo.toml | head -1 | cut -d'"' -f 2) sed -i "s/{ version = \"=$old_tag\", path/{ version = \"=$new_tag\", path/g" Cargo.toml sed -i "s/version = \"$old_tag\"/version = \"$new_tag\"/g" Cargo.toml -git add Cargo.toml -cargo check -git add Cargo.lock # Update the submodules git submodule update --remote -git add crates/utils/translations + +# Run check to ensure translations are valid and lockfile is updated +cargo check # The commit +git add Cargo.toml Cargo.lock crates/utils/translations git commit -m"Version $new_tag" git tag $new_tag diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index 8fab1b148..4c574ecf4 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.rs @@ -137,9 +137,13 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .wrap(rate_limit.image()) .route(post().to(upload_image)), ) - .service(resource("/pictrs/image/{filename}").route(get().to(get_image))) - .service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete_image))) - .service(resource("/pictrs/healthz").route(get().to(pictrs_health))) + .service( + scope("/pictrs") + .wrap(rate_limit.message()) + .route("/image/{filename}", get().to(get_image)) + .route("/image/delete/{token}/{filename}", get().to(delete_image)) + .route("/healthz", get().to(pictrs_health)), + ) .service( scope("/api/v3") .route("/image_proxy", get().to(image_proxy)) @@ -165,7 +169,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { ) .service( resource("/resolve_object") - .wrap(rate_limit.message()) + .wrap(rate_limit.search()) .route(get().to(resolve_object)), ) // Community @@ -198,12 +202,17 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { ) // Post .service( - // Handle POST to /post separately to add the post() rate limitter resource("/post") + // Handle POST to /post separately to add the post() rate limitter .guard(guard::Post()) .wrap(rate_limit.post()) .route(post().to(create_post)), ) + .service( + resource("/post/site_metadata") + .wrap(rate_limit.search()) + .route(get().to(get_link_metadata)), + ) .service( scope("/post") .wrap(rate_limit.message()) @@ -220,8 +229,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .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("/site_metadata", get().to(get_link_metadata)), + .route("/report/resolve", put().to(resolve_post_report)), ) // Comment .service( diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index 852e868fd..985d3ad2f 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -44,6 +44,7 @@ use lemmy_api::{ unread_count::unread_count, }, report_count::report_count, + resend_verification_email::resend_verification_email, reset_password::reset_password, save_settings::save_user_settings, update_totp::update_totp, @@ -65,6 +66,7 @@ use lemmy_api::{ private_message::mark_read::mark_pm_as_read, reports::{ comment_report::{create::create_comment_report, resolve::resolve_comment_report}, + community_report::{create::create_community_report, resolve::resolve_community_report}, post_report::{create::create_post_report, resolve::resolve_post_report}, private_message_report::{create::create_pm_report, resolve::resolve_pm_report}, report_combined::list::list_reports, @@ -142,7 +144,7 @@ use lemmy_api_crud::{ }, }; use lemmy_apub::api::{ - list_comments::list_comments, + list_comments::{list_comments, list_comments_slim}, list_person_content::list_person_content, list_posts::list_posts, read_community::get_community, @@ -196,7 +198,11 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .wrap(rate_limit.search()) .route(get().to(search)), ) - .route("/resolve_object", get().to(resolve_object)) + .service( + resource("/resolve_object") + .wrap(rate_limit.search()) + .route(get().to(resolve_object)), + ) // Community .service( resource("/community") @@ -212,6 +218,8 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/hide", put().to(hide_community)) .route("/list", get().to(list_communities)) .route("/follow", post().to(follow_community)) + .route("/report", post().to(create_community_report)) + .route("/report/resolve", put().to(resolve_community_report)) .route("/delete", post().to(delete_community)) // Mod Actions .route("/remove", post().to(remove_community)) @@ -232,12 +240,17 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/federated_instances", get().to(get_federated_instances)) // Post .service( - // Handle POST to /post separately to add the post() rate limitter resource("/post") + // Handle POST to /post separately to add the post() rate limitter .guard(guard::Post()) .wrap(rate_limit.post()) .route(post().to(create_post)), ) + .service( + resource("/post/site_metadata") + .wrap(rate_limit.search()) + .route(get().to(get_link_metadata)), + ) .service( scope("/post") .route("", get().to(get_post)) @@ -254,8 +267,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .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("/site_metadata", get().to(get_link_metadata)), + .route("/report/resolve", put().to(resolve_post_report)), ) // Comment .service( @@ -277,6 +289,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/like/list", get().to(list_comment_likes)) .route("/save", put().to(save_comment)) .route("/list", get().to(list_comments)) + .route("/list/slim", get().to(list_comments_slim)) .route("/report", post().to(create_comment_report)) .route("/report/resolve", put().to(resolve_comment_report)), ) @@ -311,6 +324,10 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/totp/generate", post().to(generate_totp_secret)) .route("/totp/update", post().to(update_totp)) .route("/verify_email", post().to(verify_email)) + .route( + "/resend_verification_email", + post().to(resend_verification_email), + ) .route("/saved", get().to(list_person_saved)), ) .service( diff --git a/src/lib.rs b/src/lib.rs index bd84d0264..6c660175b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,7 @@ pub mod api_routes_v3; pub mod api_routes_v4; -pub mod code_migrations; -pub mod prometheus_metrics; -pub mod scheduled_tasks; -pub mod session_middleware; -use crate::{code_migrations::run_advanced_migrations, session_middleware::SessionMiddleware}; use activitypub_federation::config::{FederationConfig, FederationMiddleware}; -use actix_cors::Cors; use actix_web::{ dev::{ServerHandle, ServiceResponse}, middleware::{self, Condition, ErrorHandlerResponse, ErrorHandlers}, @@ -16,7 +10,6 @@ use actix_web::{ HttpResponse, HttpServer, }; -use actix_web_prom::PrometheusMetricsBuilder; use clap::{Parser, Subcommand}; use lemmy_api::sitemap::get_sitemap; use lemmy_api_common::{ @@ -37,7 +30,21 @@ use lemmy_apub::{ }; use lemmy_db_schema::{schema_setup, source::secret::Secret, utils::build_db_pool}; use lemmy_federate::{Opts, SendManager}; -use lemmy_routes::{feeds, nodeinfo, webfinger}; +use lemmy_routes::{ + feeds, + middleware::{ + idempotency::{IdempotencyMiddleware, IdempotencySet}, + session::SessionMiddleware, + }, + nodeinfo, + utils::{ + code_migrations::run_advanced_migrations, + cors_config, + prometheus_metrics::{new_prometheus_metrics, serve_prometheus}, + scheduled_tasks, + }, + webfinger, +}; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, rate_limit::RateLimitCell, @@ -45,8 +52,7 @@ use lemmy_utils::{ settings::{structs::Settings, SETTINGS}, VERSION, }; -use prometheus::default_registry; -use prometheus_metrics::serve_prometheus; +use mimalloc::MiMalloc; use reqwest_middleware::ClientBuilder; use reqwest_tracing::TracingMiddleware; use serde_json::json; @@ -54,6 +60,9 @@ use std::{ops::Deref, time::Duration}; use tokio::signal::unix::SignalKind; use tracing_actix_web::{DefaultRootSpanBuilder, TracingLogger}; +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + /// Timeout for HTTP requests while sending activities. A longer timeout provides better /// compatibility with other ActivityPub software that might allocate more time for synchronous /// processing of incoming activities. This timeout should be slightly longer than the time we @@ -133,9 +142,6 @@ enum MigrationSubcommand { /// Placing the main function in lib.rs allows other crates to import it and embed Lemmy pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { - // Print version number to log - println!("Starting Lemmy v{VERSION}"); - if let Some(CmdSubcommand::Migration { subcommand, all, @@ -145,7 +151,8 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { let mut options = match subcommand { MigrationSubcommand::Run => schema_setup::Options::default().run(), MigrationSubcommand::Revert => schema_setup::Options::default().revert(), - }; + } + .print_output(); if !all { options = options.limit(number); @@ -156,6 +163,9 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { return Ok(()); } + // Print version number to log + println!("Starting Lemmy v{VERSION}"); + // return error 503 while running db migrations and startup tasks let mut startup_server_handle = None; if !args.disable_http_server { @@ -173,19 +183,17 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { // Make sure the local site is set up. let site_view = SiteView::read_local(&mut (&pool).into()).await?; - let local_site = site_view.local_site; - let federation_enabled = local_site.federation_enabled; + let federation_enabled = site_view.local_site.federation_enabled; if federation_enabled { println!("Federation enabled, host is {}", &SETTINGS.hostname); } - check_private_instance_and_federation_enabled(&local_site)?; - // Set up the rate limiter let rate_limit_config = local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); let rate_limit_cell = RateLimitCell::new(rate_limit_config); + check_private_instance_and_federation_enabled(&site_view.local_site)?; println!( "Starting HTTP server at {}:{}", @@ -203,7 +211,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { client.clone(), pictrs_client, secret.clone(), - rate_limit_cell.clone(), + rate_limit_cell, ); if let Some(prometheus) = SETTINGS.prometheus.clone() { @@ -219,8 +227,8 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { .debug(cfg!(debug_assertions)) .http_signature_compat(true) .url_verifier(Box::new(VerifyUrlData(context.inner_pool().clone()))); - if local_site.federation_signed_fetch { - let site: ApubSite = site_view.site.into(); + if site_view.local_site.federation_signed_fetch { + let site: ApubSite = site_view.site.clone().into(); federation_config_builder.signed_fetch_actor(&site); } let federation_config = federation_config_builder.build().await?; @@ -327,12 +335,9 @@ fn create_http_server( settings: Settings, federation_enabled: bool, ) -> LemmyResult { - // this must come before the HttpServer creation - // creates a middleware that populates http metrics for each path, method, and status code - let prom_api_metrics = PrometheusMetricsBuilder::new("lemmy_api") - .registry(default_registry().clone()) - .build() - .map_err(|e| LemmyErrorType::Unknown(format!("Should always be buildable: {e}")))?; + // These must come before HttpServer creation so they can collect data across threads. + let prom_api_metrics = new_prometheus_metrics()?; + let idempotency_set = IdempotencySet::default(); // Create Http server let bind = (settings.bind, settings.port); @@ -353,8 +358,8 @@ fn create_http_server( .wrap(TracingLogger::::new()) .wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors)) .app_data(Data::new(context.clone())) - .app_data(Data::new(rate_limit_cell.clone())) .wrap(FederationMiddleware::new(federation_config.clone())) + .wrap(IdempotencyMiddleware::new(idempotency_set.clone())) .wrap(SessionMiddleware::new(context.clone())) .wrap(Condition::new( SETTINGS.prometheus.is_some(), @@ -386,50 +391,3 @@ fn create_http_server( tokio::task::spawn(server); Ok(handle) } - -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 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, - } -} - -#[cfg(test)] -pub mod tests { - use activitypub_federation::config::Data; - use lemmy_api_common::context::LemmyContext; - use std::env::set_current_dir; - - pub async fn test_context() -> Data { - // hack, necessary so that config file can be loaded from hardcoded, relative path. - // Ignore errors as this gets called once for every test (so changing dir again would fail). - set_current_dir("crates/utils").ok(); - - LemmyContext::init_test_context().await - } -} diff --git a/src/main.rs b/src/main.rs index 73cd0c1a3..e030f31b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ use clap::Parser; use lemmy_server::{start_lemmy_server, CmdArgs}; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::{ + error::{LemmyErrorType, LemmyResult}, + settings::SETTINGS, +}; use tracing::level_filters::LevelFilter; use tracing_subscriber::EnvFilter; @@ -11,7 +14,14 @@ pub async fn main() -> LemmyResult<()> { let filter = EnvFilter::builder() .with_default_directive(LevelFilter::INFO.into()) .from_env_lossy(); - tracing_subscriber::fmt().with_env_filter(filter).init(); + if SETTINGS.json_logging { + tracing_subscriber::fmt() + .with_env_filter(filter) + .json() + .init(); + } else { + tracing_subscriber::fmt().with_env_filter(filter).init(); + } let args = CmdArgs::parse();