diff --git a/.woodpecker.yml b/.woodpecker.yml index c218460dd..e832639e0 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -2,7 +2,7 @@ # See https://github.com/woodpecker-ci/woodpecker/issues/1677 variables: - - &rust_image "rust:1.77" + - &rust_image "rust:1.78" - &rust_nightly_image "rustlang/rust:nightly" - &install_pnpm "corepack enable pnpm" - &slow_check_paths diff --git a/Cargo.lock b/Cargo.lock index 82eccfe61..02c62ca5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,11 +250,9 @@ dependencies = [ "pin-project-lite", "rustls-pki-types", "tokio", - "tokio-rustls 0.23.4", "tokio-rustls 0.26.0", "tokio-util", "tracing", - "webpki-roots 0.22.6", ] [[package]] @@ -980,9 +978,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -990,9 +988,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -1002,9 +1000,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1110,18 +1108,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "console-api" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" -dependencies = [ - "prost 0.11.9", - "prost-types 0.11.9", - "tonic 0.9.2", - "tracing-core", -] - [[package]] name = "console-api" version = "0.6.0" @@ -1130,33 +1116,22 @@ checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" dependencies = [ "futures-core", "prost 0.12.6", - "prost-types 0.12.6", + "prost-types", "tonic 0.10.2", "tracing-core", ] [[package]] -name = "console-subscriber" -version = "0.1.10" +name = "console-api" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" +checksum = "a257c22cd7e487dd4a13d413beabc512c5052f0bc048db0da6a84c3d8a6142fd" dependencies = [ - "console-api 0.5.0", - "crossbeam-channel", - "crossbeam-utils", - "futures", - "hdrhistogram", - "humantime", - "prost-types 0.11.9", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic 0.9.2", - "tracing", + "futures-core", + "prost 0.12.6", + "prost-types", + "tonic 0.11.0", "tracing-core", - "tracing-subscriber", ] [[package]] @@ -1171,7 +1146,7 @@ dependencies = [ "futures-task", "hdrhistogram", "humantime", - "prost-types 0.12.6", + "prost-types", "serde", "serde_json", "thread_local", @@ -1183,6 +1158,31 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "console-subscriber" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c4cc54bae66f7d9188996404abdf7fdfa23034ef8e43478c8810828abad758" +dependencies = [ + "console-api 0.7.0", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost 0.12.6", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic 0.11.0", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1461,6 +1461,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-new" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "derive_builder" version = "0.20.0" @@ -2711,7 +2722,7 @@ dependencies = [ "base64 0.21.7", "js-sys", "pem", - "ring 0.17.8", + "ring", "serde", "serde_json", "simple_asn1", @@ -2737,7 +2748,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lemmy_api" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "activitypub_federation", "actix-web", @@ -2748,6 +2759,7 @@ dependencies = [ "captcha", "chrono", "elementtree", + "hound", "lemmy_api_common", "lemmy_db_schema", "lemmy_db_views", @@ -2761,12 +2773,11 @@ dependencies = [ "totp-rs", "tracing", "url", - "wav", ] [[package]] name = "lemmy_api_common" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "activitypub_federation", "actix-web", @@ -2804,7 +2815,7 @@ dependencies = [ [[package]] name = "lemmy_api_crud" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "accept-language", "activitypub_federation", @@ -2827,7 +2838,7 @@ dependencies = [ [[package]] name = "lemmy_apub" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "activitypub_federation", "actix-web", @@ -2865,7 +2876,7 @@ dependencies = [ [[package]] name = "lemmy_db_perf" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "anyhow", "clap", @@ -2880,7 +2891,7 @@ dependencies = [ [[package]] name = "lemmy_db_schema" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "activitypub_federation", "anyhow", @@ -2888,6 +2899,7 @@ dependencies = [ "bcrypt", "chrono", "deadpool 0.12.1", + "derive-new", "diesel", "diesel-async", "diesel-derive-enum", @@ -2902,7 +2914,7 @@ dependencies = [ "once_cell", "pretty_assertions", "regex", - "rustls 0.23.8", + "rustls 0.23.10", "serde", "serde_json", "serde_with", @@ -2921,7 +2933,7 @@ dependencies = [ [[package]] name = "lemmy_db_views" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "actix-web", "chrono", @@ -2943,12 +2955,13 @@ dependencies = [ [[package]] name = "lemmy_db_views_actor" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "chrono", "diesel", "diesel-async", "lemmy_db_schema", + "lemmy_db_views", "lemmy_utils", "pretty_assertions", "serde", @@ -2963,7 +2976,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_moderator" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "diesel", "diesel-async", @@ -2975,7 +2988,7 @@ dependencies = [ [[package]] name = "lemmy_federate" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "activitypub_federation", "anyhow", @@ -3000,7 +3013,7 @@ dependencies = [ [[package]] name = "lemmy_routes" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "activitypub_federation", "actix-web", @@ -3025,7 +3038,7 @@ dependencies = [ [[package]] name = "lemmy_server" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "activitypub_federation", "actix-cors", @@ -3034,7 +3047,7 @@ dependencies = [ "chrono", "clap", "clokwerk", - "console-subscriber 0.1.10", + "console-subscriber 0.3.0", "diesel", "diesel-async", "futures-util", @@ -3068,7 +3081,7 @@ dependencies = [ [[package]] name = "lemmy_utils" -version = "0.19.4-rc.3" +version = "0.19.5" dependencies = [ "actix-web", "anyhow", @@ -3353,9 +3366,9 @@ checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "metrics" -version = "0.22.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be3cbd384d4e955b231c895ce10685e3d8260c5ccffae898c96c723b0772835" +checksum = "884adb57038347dfbaf2d5065887b6cf4312330dc8e94bc30a1a839bd79d3261" dependencies = [ "ahash", "portable-atomic", @@ -3363,9 +3376,9 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d58e362dc7206e9456ddbcdbd53c71ba441020e62104703075a69151e38d85f" +checksum = "bf0af7a0d7ced10c0151f870e5e3f3f8bc9ffc5992d32873566ca1f9169ae776" dependencies = [ "base64 0.22.1", "http-body-util", @@ -3383,9 +3396,9 @@ dependencies = [ [[package]] name = "metrics-util" -version = "0.16.3" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b07a5eb561b8cbc16be2d216faf7757f9baf3bfb94dbb0fae3df8387a5bb47f" +checksum = "4259040465c955f9f2f1a4a8a16dc46726169bca0f88e8fb2dbeced487c3e828" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -3685,9 +3698,9 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d57987be3f2aeb70d385fff9b27fb74c5723cc9a52d904d4f9c807a0667bf" +checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" dependencies = [ "futures-core", "futures-sink", @@ -3695,7 +3708,6 @@ dependencies = [ "once_cell", "pin-project-lite", "thiserror", - "urlencoding", ] [[package]] @@ -3718,17 +3730,16 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a016b8d9495c639af2145ac22387dcb88e44118e45320d9238fbf4e7889abcb" +checksum = "a94c69209c05319cdf7460c6d4c055ed102be242a0a6245835d7bc42c6ec7f54" dependencies = [ "async-trait", "futures-core", "http 0.2.12", - "opentelemetry 0.22.0", - "opentelemetry-proto 0.5.0", - "opentelemetry-semantic-conventions", - "opentelemetry_sdk 0.22.1", + "opentelemetry 0.23.0", + "opentelemetry-proto 0.6.0", + "opentelemetry_sdk 0.23.0", "prost 0.12.6", "thiserror", "tokio", @@ -3750,22 +3761,16 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8fddc9b68f5b80dae9d6f510b88e02396f006ad48cac349411fbecc80caae4" +checksum = "984806e6cf27f2b49282e2a05e288f30594f3dbc74eb7a6e99422bc48ed78162" dependencies = [ - "opentelemetry 0.22.0", - "opentelemetry_sdk 0.22.1", + "opentelemetry 0.23.0", + "opentelemetry_sdk 0.23.0", "prost 0.12.6", "tonic 0.11.0", ] -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9ab5bd6c42fb9349dcf28af2ba9a0667f697f9bdcca045d39f2cec5543e2910" - [[package]] name = "opentelemetry_api" version = "0.19.0" @@ -3806,18 +3811,18 @@ dependencies = [ [[package]] name = "opentelemetry_sdk" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e90c7113be649e31e9a0f8b5ee24ed7a16923b322c3c5ab6367469c049d6b7e" +checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd" dependencies = [ "async-trait", - "crossbeam-channel", "futures-channel", "futures-executor", "futures-util", "glob", + "lazy_static", "once_cell", - "opentelemetry 0.22.0", + "opentelemetry 0.23.0", "ordered-float", "percent-encoding", "rand", @@ -4016,9 +4021,9 @@ dependencies = [ [[package]] name = "pict-rs" -version = "0.5.14" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cb4fd629bb8a115b8ae2701e2c355f6e5e9ad6bf22d9d0e7b7b645fb0f81dc" +checksum = "5bbee61836cce10f7cf733196b7c0701e7ea6d0b617da68a3e6e4311b6262c2b" dependencies = [ "actix-form-data", "actix-web", @@ -4041,15 +4046,15 @@ dependencies = [ "metrics", "metrics-exporter-prometheus", "mime", - "opentelemetry 0.22.0", - "opentelemetry-otlp 0.15.0", - "opentelemetry_sdk 0.22.1", + "opentelemetry 0.23.0", + "opentelemetry-otlp 0.16.0", + "opentelemetry_sdk 0.23.0", "pin-project-lite", "refinery", "reqwest 0.12.4", "reqwest-middleware 0.3.1", "reqwest-tracing 0.5.0", - "rustls 0.23.8", + "rustls 0.23.10", "rustls-channel-resolver", "rustls-pemfile 2.1.2", "rusty-s3", @@ -4072,11 +4077,11 @@ dependencies = [ "tracing-actix-web", "tracing-error", "tracing-log 0.2.0", - "tracing-opentelemetry 0.23.0", + "tracing-opentelemetry 0.24.0", "tracing-subscriber", "url", "uuid", - "webpki-roots 0.26.1", + "webpki-roots", ] [[package]] @@ -4339,15 +4344,6 @@ dependencies = [ "syn 2.0.65", ] -[[package]] -name = "prost-types" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" -dependencies = [ - "prost 0.11.9", -] - [[package]] name = "prost-types" version = "0.12.6" @@ -4678,7 +4674,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.1", + "webpki-roots", "winreg 0.52.0", ] @@ -4761,27 +4757,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "riff" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b1a3d5f46d53f4a3478e2be4a5a5ce5108ea58b100dcd139830eae7f79a3a1" - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -4792,7 +4767,7 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -4875,18 +4850,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.22.4" @@ -4894,7 +4857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", - "ring 0.17.8", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -4903,14 +4866,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.8" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79adb16721f56eb2d843e67676896a61ce7a0fa622dc18d3e372477a029d2740" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring 0.17.8", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -4924,7 +4887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fede2a247359da6b4998f7723ec6468c2d6a577a5d8c17e54f21806426ad2290" dependencies = [ "nanorand", - "rustls 0.23.8", + "rustls 0.23.10", ] [[package]] @@ -4959,7 +4922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "aws-lc-rs", - "ring 0.17.8", + "ring", "rustls-pki-types", "untrusted 0.9.0", ] @@ -5038,16 +5001,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "sdd" version = "0.2.0" @@ -5096,9 +5049,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -5114,9 +5067,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -5373,12 +5326,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -5487,11 +5434,11 @@ checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -5727,9 +5674,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -5757,9 +5704,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -5809,7 +5756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8e98c31c29b2666fb28720739e11476166be4ead1610a37dcd7414bb124413a" dependencies = [ "aws-lc-rs", - "rustls 0.23.8", + "rustls 0.23.10", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", @@ -5822,25 +5769,14 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" dependencies = [ - "ring 0.17.8", - "rustls 0.23.8", + "ring", + "rustls 0.23.10", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", "x509-certificate", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - [[package]] name = "tokio-rustls" version = "0.25.0" @@ -5858,7 +5794,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.8", + "rustls 0.23.10", "rustls-pki-types", "tokio", ] @@ -5978,34 +5914,6 @@ dependencies = [ "tracing-futures", ] -[[package]] -name = "tonic" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" -dependencies = [ - "async-trait", - "axum", - "base64 0.21.7", - "bytes", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", - "hyper-timeout", - "percent-encoding", - "pin-project", - "prost 0.11.9", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic" version = "0.10.2" @@ -6122,16 +6030,16 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa069bd1503dd526ee793bb3fce408895136c95fc86d2edb2acf1c646d7f0684" +checksum = "4ee9e39a66d9b615644893ffc1704d2a89b5b315b7fd0228ad3182ca9a306b19" dependencies = [ "actix-web", "mutually_exclusive_features", - "opentelemetry 0.22.0", + "opentelemetry 0.23.0", "pin-project", "tracing", - "tracing-opentelemetry 0.23.0", + "tracing-opentelemetry 0.24.0", "uuid", ] @@ -6227,14 +6135,14 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9be14ba1bbe4ab79e9229f7f89fab8d120b865859f10527f31c033e599d2284" +checksum = "f68803492bf28ab40aeccaecc7021096bd256baf7ca77c3d425d89b35a7be4e4" dependencies = [ "js-sys", "once_cell", - "opentelemetry 0.22.0", - "opentelemetry_sdk 0.22.1", + "opentelemetry 0.23.0", + "opentelemetry_sdk 0.23.0", "smallvec", "tracing", "tracing-core", @@ -6571,15 +6479,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wav" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d97402f69875b579ec37f2aa52d1f455a1d6224251edba32e8c18a5da2698d" -dependencies = [ - "riff", -] - [[package]] name = "web-sys" version = "0.3.69" @@ -6628,25 +6527,6 @@ dependencies = [ "url", ] -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - [[package]] name = "webpki-roots" version = "0.26.1" @@ -6930,7 +6810,7 @@ dependencies = [ "der", "hex", "pem", - "ring 0.17.8", + "ring", "signature", "spki", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 21ca80069..ce6d3357d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.19.4-rc.3" +version = "0.19.5" edition = "2021" description = "A link aggregator for the fediverse" license = "AGPL-3.0" @@ -88,35 +88,35 @@ unused_self = "deny" unwrap_used = "deny" [workspace.dependencies] -lemmy_api = { version = "=0.19.4-rc.3", path = "./crates/api" } -lemmy_api_crud = { version = "=0.19.4-rc.3", path = "./crates/api_crud" } -lemmy_apub = { version = "=0.19.4-rc.3", path = "./crates/apub" } -lemmy_utils = { version = "=0.19.4-rc.3", path = "./crates/utils", default-features = false } -lemmy_db_schema = { version = "=0.19.4-rc.3", path = "./crates/db_schema" } -lemmy_api_common = { version = "=0.19.4-rc.3", path = "./crates/api_common" } -lemmy_routes = { version = "=0.19.4-rc.3", path = "./crates/routes" } -lemmy_db_views = { version = "=0.19.4-rc.3", path = "./crates/db_views" } -lemmy_db_views_actor = { version = "=0.19.4-rc.3", path = "./crates/db_views_actor" } -lemmy_db_views_moderator = { version = "=0.19.4-rc.3", path = "./crates/db_views_moderator" } -lemmy_federate = { version = "=0.19.4-rc.3", path = "./crates/federate" } +lemmy_api = { version = "=0.19.5", path = "./crates/api" } +lemmy_api_crud = { version = "=0.19.5", path = "./crates/api_crud" } +lemmy_apub = { version = "=0.19.5", path = "./crates/apub" } +lemmy_utils = { version = "=0.19.5", path = "./crates/utils", default-features = false } +lemmy_db_schema = { version = "=0.19.5", path = "./crates/db_schema" } +lemmy_api_common = { version = "=0.19.5", path = "./crates/api_common" } +lemmy_routes = { version = "=0.19.5", path = "./crates/routes" } +lemmy_db_views = { version = "=0.19.5", path = "./crates/db_views" } +lemmy_db_views_actor = { version = "=0.19.5", path = "./crates/db_views_actor" } +lemmy_db_views_moderator = { version = "=0.19.5", path = "./crates/db_views_moderator" } +lemmy_federate = { version = "=0.19.5", path = "./crates/federate" } activitypub_federation = { version = "0.5.6", default-features = false, features = [ "actix-web", ] } diesel = "2.1.6" diesel_migrations = "2.1.0" diesel-async = "0.4.1" -serde = { version = "1.0.202", features = ["derive"] } +serde = { version = "1.0.203", features = ["derive"] } serde_with = "3.8.1" actix-web = { version = "4.6.0", default-features = false, features = [ "macros", - "rustls", + "rustls-0_23", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", ] } tracing = "0.1.40" -tracing-actix-web = { version = "0.7.10", default-features = false } +tracing-actix-web = { version = "0.7.11", default-features = false } tracing-error = "0.2.0" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } @@ -139,13 +139,13 @@ anyhow = { version = "1.0.86", features = [ diesel_ltree = "0.3.1" typed-builder = "0.18.2" serial_test = "3.1.1" -tokio = { version = "1.37.0", features = ["full"] } +tokio = { version = "1.38.0", features = ["full"] } regex = "1.10.4" once_cell = "1.19.0" diesel-derive-newtype = "2.1.2" diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } strum = "0.26.2" -strum_macros = "0.26.2" +strum_macros = "0.26.4" itertools = "0.13.0" futures = "0.3.30" http = "0.2.12" @@ -157,7 +157,7 @@ ts-rs = { version = "7.1.1", features = [ "chrono-impl", "no-serde-warnings", ] } -rustls = { version = "0.23.8", features = ["ring"] } +rustls = { version = "0.23.9", features = ["ring"] } futures-util = "0.3.30" tokio-postgres = "0.7.10" tokio-postgres-rustls = "0.12.0" @@ -165,8 +165,9 @@ urlencoding = "2.1.3" enum-map = "2.7" moka = { version = "0.12.7", features = ["future"] } i-love-jesus = { version = "0.1.0" } -clap = { version = "4.5.4", features = ["derive", "env"] } +clap = { version = "4.5.6", features = ["derive", "env"] } pretty_assertions = "1.4.0" +derive-new = "0.6.0" [dependencies] lemmy_api = { workspace = true } @@ -194,9 +195,9 @@ clokwerk = { workspace = true } serde_json = { workspace = true } tracing-opentelemetry = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true } -console-subscriber = { version = "0.1.10", optional = true } +console-subscriber = { version = "0.3.0", optional = true } opentelemetry-otlp = { version = "0.12.0", optional = true } -pict-rs = { version = "0.5.14", optional = true } +pict-rs = { version = "0.5.15", optional = true } tokio.workspace = true actix-cors = "0.7.0" futures-util = { workspace = true } diff --git a/api_tests/.eslintrc.json b/api_tests/.eslintrc.json deleted file mode 100644 index 75b1706aa..000000000 --- a/api_tests/.eslintrc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "root": true, - "env": { - "browser": true - }, - "plugins": ["@typescript-eslint"], - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "warnOnUnsupportedTypeScriptVersion": false - }, - "rules": { - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/explicit-module-boundary-types": 0, - "@typescript-eslint/no-var-requires": 0, - "arrow-body-style": 0, - "curly": 0, - "eol-last": 0, - "eqeqeq": 0, - "func-style": 0, - "import/no-duplicates": 0, - "max-statements": 0, - "max-params": 0, - "new-cap": 0, - "no-console": 0, - "no-duplicate-imports": 0, - "no-extra-parens": 0, - "no-return-assign": 0, - "no-throw-literal": 0, - "no-trailing-spaces": 0, - "no-unused-expressions": 0, - "no-useless-constructor": 0, - "no-useless-escape": 0, - "no-var": 0, - "prefer-const": 0, - "prefer-rest-params": 0, - "quote-props": 0, - "unicorn/filename-case": 0 - } -} diff --git a/api_tests/eslint.config.mjs b/api_tests/eslint.config.mjs new file mode 100644 index 000000000..cf2c426d0 --- /dev/null +++ b/api_tests/eslint.config.mjs @@ -0,0 +1,56 @@ +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + parser: tseslint.parser, + }, + }, + // For some reason this has to be in its own block + { + ignores: [ + "putTypesInIndex.js", + "dist/*", + "docs/*", + ".yalc", + "jest.config.js", + ], + }, + { + files: ["src/**/*"], + rules: { + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-var-requires": 0, + "arrow-body-style": 0, + curly: 0, + "eol-last": 0, + eqeqeq: 0, + "func-style": 0, + "import/no-duplicates": 0, + "max-statements": 0, + "max-params": 0, + "new-cap": 0, + "no-console": 0, + "no-duplicate-imports": 0, + "no-extra-parens": 0, + "no-return-assign": 0, + "no-throw-literal": 0, + "no-trailing-spaces": 0, + "no-unused-expressions": 0, + "no-useless-constructor": 0, + "no-useless-escape": 0, + "no-var": 0, + "prefer-const": 0, + "prefer-rest-params": 0, + "quote-props": 0, + "unicorn/filename-case": 0, + }, + }, +]; diff --git a/api_tests/package.json b/api_tests/package.json index b194dae30..6a14bded7 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -6,9 +6,9 @@ "repository": "https://github.com/LemmyNet/lemmy", "author": "Dessalines", "license": "AGPL-3.0", - "packageManager": "pnpm@9.1.1+sha256.9551e803dcb7a1839fdf5416153a844060c7bce013218ce823410532504ac10b", + "packageManager": "pnpm@9.4.0", "scripts": { - "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'", + "lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'", "fix": "prettier --write src && eslint --fix src", "api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ", "api-test-follow": "jest -i follow.spec.ts", @@ -25,12 +25,13 @@ "@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/parser": "^7.5.0", "download-file-sync": "^1.0.4", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.19.4-alpha.18", + "lemmy-js-client": "0.19.5-alpha.1", "prettier": "^3.2.5", "ts-jest": "^29.1.0", - "typescript": "^5.4.4" + "typescript": "^5.4.4", + "typescript-eslint": "^7.13.0" } } diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 42eb612e5..31ec952c1 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -13,37 +13,40 @@ importers: version: 29.5.12 '@types/node': specifier: ^20.12.4 - version: 20.12.4 + version: 20.14.5 '@typescript-eslint/eslint-plugin': specifier: ^7.5.0 - version: 7.5.0(@typescript-eslint/parser@7.5.0)(eslint@8.57.0)(typescript@5.4.4) + version: 7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5) '@typescript-eslint/parser': specifier: ^7.5.0 - version: 7.5.0(eslint@8.57.0)(typescript@5.4.4) + version: 7.13.1(eslint@9.5.0)(typescript@5.4.5) download-file-sync: specifier: ^1.0.4 version: 1.0.4 eslint: - specifier: ^8.57.0 - version: 8.57.0 + specifier: ^9.0.0 + version: 9.5.0 eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.1.3(eslint@8.57.0)(prettier@3.2.5) + version: 5.1.3(eslint@9.5.0)(prettier@3.3.2) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.12.4) + version: 29.7.0(@types/node@20.14.5) lemmy-js-client: - specifier: 0.19.4-alpha.18 - version: 0.19.4-alpha.18 + specifier: 0.19.5-alpha.1 + version: 0.19.5-alpha.1 prettier: specifier: ^3.2.5 - version: 3.2.5 + version: 3.3.2 ts-jest: specifier: ^29.1.0 - version: 29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.4.4) + version: 29.1.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@20.14.5))(typescript@5.4.5) typescript: specifier: ^5.4.4 - version: 5.4.4 + version: 5.4.5 + typescript-eslint: + specifier: ^7.13.0 + version: 7.13.0(eslint@9.5.0)(typescript@5.4.5) packages: @@ -232,24 +235,33 @@ packages: resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint-community/regexpp@4.10.1': + resolution: {integrity: sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/js@8.57.0': - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-array@0.16.0': + resolution: {integrity: sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@humanwhocodes/config-array@0.11.14': - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.5.0': + resolution: {integrity: sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.2': - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + '@humanwhocodes/retry@0.3.0': + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} @@ -395,14 +407,8 @@ packages: '@types/jest@29.5.12': resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/node@20.12.4': - resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==} - - '@types/semver@7.5.8': - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/node@20.14.5': + resolution: {integrity: sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -413,8 +419,8 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@7.5.0': - resolution: {integrity: sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==} + '@typescript-eslint/eslint-plugin@7.13.0': + resolution: {integrity: sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 @@ -424,8 +430,19 @@ packages: typescript: optional: true - '@typescript-eslint/parser@7.5.0': - resolution: {integrity: sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==} + '@typescript-eslint/eslint-plugin@7.13.1': + resolution: {integrity: sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.13.0': + resolution: {integrity: sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -434,12 +451,8 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@7.5.0': - resolution: {integrity: sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/type-utils@7.5.0': - resolution: {integrity: sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==} + '@typescript-eslint/parser@7.13.1': + resolution: {integrity: sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -448,12 +461,44 @@ packages: typescript: optional: true - '@typescript-eslint/types@7.5.0': - resolution: {integrity: sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==} + '@typescript-eslint/scope-manager@7.13.0': + resolution: {integrity: sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/typescript-estree@7.5.0': - resolution: {integrity: sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==} + '@typescript-eslint/scope-manager@7.13.1': + resolution: {integrity: sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.13.0': + resolution: {integrity: sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/type-utils@7.13.1': + resolution: {integrity: sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.13.0': + resolution: {integrity: sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/types@7.13.1': + resolution: {integrity: sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.13.0': + resolution: {integrity: sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: typescript: '*' @@ -461,18 +506,34 @@ packages: typescript: optional: true - '@typescript-eslint/utils@7.5.0': - resolution: {integrity: sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==} + '@typescript-eslint/typescript-estree@7.13.1': + resolution: {integrity: sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.13.0': + resolution: {integrity: sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 - '@typescript-eslint/visitor-keys@7.5.0': - resolution: {integrity: sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==} + '@typescript-eslint/utils@7.13.1': + resolution: {integrity: sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.13.0': + resolution: {integrity: sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==} engines: {node: ^18.18.0 || >=20.0.0} - '@ungap/structured-clone@1.2.0': - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@typescript-eslint/visitor-keys@7.13.1': + resolution: {integrity: sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==} + engines: {node: ^18.18.0 || >=20.0.0} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -559,6 +620,10 @@ packages: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + browserslist@4.22.3: resolution: {integrity: sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -656,6 +721,15 @@ packages: supports-color: optional: true + debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} peerDependencies: @@ -683,10 +757,6 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - download-file-sync@1.0.4: resolution: {integrity: sha512-vH92qNH508jZZA12HQNq/aiMDfagr4JvjFiI17Bi8oYjsxwv5ZVIi7iHkYmUXxOQUr90tcVX+8EPePjAqG1Y0w==} @@ -733,22 +803,26 @@ packages: eslint-config-prettier: optional: true - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.0.1: + resolution: {integrity: sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.5.0: + resolution: {integrity: sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.0.1: + resolution: {integrity: sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} @@ -805,14 +879,18 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -821,9 +899,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} @@ -865,14 +943,15 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -922,6 +1001,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1156,8 +1236,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.19.4-alpha.18: - resolution: {integrity: sha512-CUKRIiINZF2zOfK5WzBDF071LjMmRBFHwiSYBMGJyQP1zu8sPKCb/ptg25WWrf79Y4uOaVLctgHg3oEUXmSUmQ==} + lemmy-js-client@0.19.5-alpha.1: + resolution: {integrity: sha512-GOhaiTQzrpwdmc3DFYemT2SmNmpuQJe2BWUms9QOzdYlkA1WZ0uu7axPE3s+T5OOxfy7K9Q2gsLe72dcVSlffw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -1187,10 +1267,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -1212,6 +1288,10 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1219,8 +1299,8 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} engines: {node: '>=16 || 14 >=14.17'} ms@2.1.2: @@ -1324,8 +1404,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + prettier@3.3.2: + resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} engines: {node: '>=14'} hasBin: true @@ -1378,10 +1458,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1389,13 +1465,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - - semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} engines: {node: '>=10'} hasBin: true @@ -1499,12 +1570,13 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-jest@29.1.2: - resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} - engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} + ts-jest@29.1.5: + resolution: {integrity: sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 '@jest/types': ^29.0.0 babel-jest: ^29.0.0 esbuild: '*' @@ -1513,6 +1585,8 @@ packages: peerDependenciesMeta: '@babel/core': optional: true + '@jest/transform': + optional: true '@jest/types': optional: true babel-jest: @@ -1531,16 +1605,22 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript@5.4.4: - resolution: {integrity: sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==} + typescript-eslint@7.13.0: + resolution: {integrity: sha512-upO0AXxyBwJ4BbiC6CRgAJKtGYha2zw4m1g7TIVPSonwYEuf7vCicw3syjS1OxdDMTz96sZIXl3Jx3vWJLLKFw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true @@ -1586,9 +1666,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -1630,7 +1707,7 @@ snapshots: '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.5 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1796,7 +1873,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - debug: 4.3.4 + debug: 4.3.5 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1809,19 +1886,29 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.5.0)': dependencies: - eslint: 8.57.0 + eslint: 9.5.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.10.0': {} - '@eslint/eslintrc@2.1.4': + '@eslint-community/regexpp@4.10.1': {} + + '@eslint/config-array@0.16.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.6.1 - globals: 13.24.0 + espree: 10.0.1 + globals: 14.0.0 ignore: 5.3.1 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -1830,19 +1917,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.0': {} + '@eslint/js@9.5.0': {} - '@humanwhocodes/config-array@0.11.14': - dependencies: - '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@eslint/object-schema@2.1.4': {} '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.2': {} + '@humanwhocodes/retry@0.3.0': {} '@istanbuljs/load-nyc-config@1.1.0': dependencies: @@ -1857,7 +1938,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1870,14 +1951,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.12.4) + jest-config: 29.7.0(@types/node@20.14.5) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1902,7 +1983,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1920,7 +2001,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.12.4 + '@types/node': 20.14.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1942,7 +2023,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 20.12.4 + '@types/node': 20.14.5 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2012,7 +2093,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.12.4 + '@types/node': 20.14.5 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2080,7 +2161,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 20.12.4 + '@types/node': 20.14.5 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2097,14 +2178,10 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 - '@types/json-schema@7.0.15': {} - - '@types/node@20.12.4': + '@types/node@20.14.5': dependencies: undici-types: 5.26.5 - '@types/semver@7.5.8': {} - '@types/stack-utils@2.0.3': {} '@types/yargs-parser@21.0.3': {} @@ -2113,89 +2190,167 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.5.0(@typescript-eslint/parser@7.5.0)(eslint@8.57.0)(typescript@5.4.4)': + '@typescript-eslint/eslint-plugin@7.13.0(@typescript-eslint/parser@7.13.0(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5)': dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.5.0(eslint@8.57.0)(typescript@5.4.4) - '@typescript-eslint/scope-manager': 7.5.0 - '@typescript-eslint/type-utils': 7.5.0(eslint@8.57.0)(typescript@5.4.4) - '@typescript-eslint/utils': 7.5.0(eslint@8.57.0)(typescript@5.4.4) - '@typescript-eslint/visitor-keys': 7.5.0 - debug: 4.3.4 - eslint: 8.57.0 + '@eslint-community/regexpp': 4.10.1 + '@typescript-eslint/parser': 7.13.0(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/scope-manager': 7.13.0 + '@typescript-eslint/type-utils': 7.13.0(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.13.0(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.13.0 + eslint: 9.5.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.4) - typescript: 5.4.4 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.4)': + '@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5)': dependencies: - '@typescript-eslint/scope-manager': 7.5.0 - '@typescript-eslint/types': 7.5.0 - '@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.4) - '@typescript-eslint/visitor-keys': 7.5.0 - debug: 4.3.4 - eslint: 8.57.0 - typescript: 5.4.4 + '@eslint-community/regexpp': 4.10.1 + '@typescript-eslint/parser': 7.13.1(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/scope-manager': 7.13.1 + '@typescript-eslint/type-utils': 7.13.1(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.13.1 + eslint: 9.5.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.5.0': + '@typescript-eslint/parser@7.13.0(eslint@9.5.0)(typescript@5.4.5)': dependencies: - '@typescript-eslint/types': 7.5.0 - '@typescript-eslint/visitor-keys': 7.5.0 - - '@typescript-eslint/type-utils@7.5.0(eslint@8.57.0)(typescript@5.4.4)': - dependencies: - '@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.4) - '@typescript-eslint/utils': 7.5.0(eslint@8.57.0)(typescript@5.4.4) - debug: 4.3.4 - eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.4) - typescript: 5.4.4 + '@typescript-eslint/scope-manager': 7.13.0 + '@typescript-eslint/types': 7.13.0 + '@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.13.0 + debug: 4.3.5 + eslint: 9.5.0 + optionalDependencies: + typescript: 5.4.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@7.5.0': {} - - '@typescript-eslint/typescript-estree@7.5.0(typescript@5.4.4)': + '@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.4.5)': dependencies: - '@typescript-eslint/types': 7.5.0 - '@typescript-eslint/visitor-keys': 7.5.0 - debug: 4.3.4 + '@typescript-eslint/scope-manager': 7.13.1 + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.13.1 + debug: 4.3.5 + eslint: 9.5.0 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.13.0': + dependencies: + '@typescript-eslint/types': 7.13.0 + '@typescript-eslint/visitor-keys': 7.13.0 + + '@typescript-eslint/scope-manager@7.13.1': + dependencies: + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/visitor-keys': 7.13.1 + + '@typescript-eslint/type-utils@7.13.0(eslint@9.5.0)(typescript@5.4.5)': + dependencies: + '@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.5) + '@typescript-eslint/utils': 7.13.0(eslint@9.5.0)(typescript@5.4.5) + debug: 4.3.5 + eslint: 9.5.0 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@7.13.1(eslint@9.5.0)(typescript@5.4.5)': + dependencies: + '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.4.5) + '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.4.5) + debug: 4.3.5 + eslint: 9.5.0 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.13.0': {} + + '@typescript-eslint/types@7.13.1': {} + + '@typescript-eslint/typescript-estree@7.13.0(typescript@5.4.5)': + dependencies: + '@typescript-eslint/types': 7.13.0 + '@typescript-eslint/visitor-keys': 7.13.0 + debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.4) - typescript: 5.4.4 + minimatch: 9.0.4 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.5.0(eslint@8.57.0)(typescript@5.4.4)': + '@typescript-eslint/typescript-estree@7.13.1(typescript@5.4.5)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.5.0 - '@typescript-eslint/types': 7.5.0 - '@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.4) - eslint: 8.57.0 - semver: 7.6.0 + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/visitor-keys': 7.13.1 + debug: 4.3.5 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.4 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.13.0(eslint@9.5.0)(typescript@5.4.5)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) + '@typescript-eslint/scope-manager': 7.13.0 + '@typescript-eslint/types': 7.13.0 + '@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.5) + eslint: 9.5.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@7.5.0': + '@typescript-eslint/utils@7.13.1(eslint@9.5.0)(typescript@5.4.5)': dependencies: - '@typescript-eslint/types': 7.5.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) + '@typescript-eslint/scope-manager': 7.13.1 + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.4.5) + eslint: 9.5.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.13.0': + dependencies: + '@typescript-eslint/types': 7.13.0 eslint-visitor-keys: 3.4.3 - '@ungap/structured-clone@1.2.0': {} + '@typescript-eslint/visitor-keys@7.13.1': + dependencies: + '@typescript-eslint/types': 7.13.1 + eslint-visitor-keys: 3.4.3 acorn-jsx@5.3.2(acorn@8.11.3): dependencies: @@ -2306,6 +2461,10 @@ snapshots: dependencies: fill-range: 7.0.1 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + browserslist@4.22.3: dependencies: caniuse-lite: 1.0.30001581 @@ -2374,13 +2533,13 @@ snapshots: convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@20.12.4): + create-jest@29.7.0(@types/node@20.14.5): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.12.4) + jest-config: 29.7.0(@types/node@20.14.5) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2399,6 +2558,10 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.3.5: + dependencies: + ms: 2.1.2 + dedent@1.5.1: {} deep-is@0.1.4: {} @@ -2413,10 +2576,6 @@ snapshots: dependencies: path-type: 4.0.0 - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - download-file-sync@1.0.4: {} electron-to-chromium@1.4.648: {} @@ -2437,52 +2596,50 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.1.3(eslint@8.57.0)(prettier@3.2.5): + eslint-plugin-prettier@5.1.3(eslint@9.5.0)(prettier@3.3.2): dependencies: - eslint: 8.57.0 - prettier: 3.2.5 + eslint: 9.5.0 + prettier: 3.3.2 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 - eslint-scope@7.2.2: + eslint-scope@8.0.1: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint@8.57.0: + eslint-visitor-keys@4.0.0: {} + + eslint@9.5.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint/config-array': 0.16.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.5.0 '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 debug: 4.3.4 - doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.0.1 + eslint-visitor-keys: 4.0.0 + espree: 10.0.1 esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.1 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 @@ -2494,11 +2651,11 @@ snapshots: transitivePeerDependencies: - supports-color - espree@9.6.1: + espree@10.0.1: dependencies: acorn: 8.11.3 acorn-jsx: 5.3.2(acorn@8.11.3) - eslint-visitor-keys: 3.4.3 + eslint-visitor-keys: 4.0.0 esprima@4.0.1: {} @@ -2546,7 +2703,7 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 + micromatch: 4.0.7 fast-json-stable-stringify@2.1.0: {} @@ -2560,14 +2717,18 @@ snapshots: dependencies: bser: 2.1.1 - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -2578,11 +2739,10 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@3.2.0: + flat-cache@4.0.1: dependencies: flatted: 3.3.1 keyv: 4.5.4 - rimraf: 3.0.2 flatted@3.3.1: {} @@ -2620,9 +2780,7 @@ snapshots: globals@11.12.0: {} - globals@13.24.0: - dependencies: - type-fest: 0.20.2 + globals@14.0.0: {} globby@11.1.0: dependencies: @@ -2712,7 +2870,7 @@ snapshots: '@babel/parser': 7.23.9 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.5.4 + semver: 7.6.2 transitivePeerDependencies: - supports-color @@ -2724,7 +2882,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.4 + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -2747,7 +2905,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -2767,16 +2925,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.12.4): + jest-cli@29.7.0(@types/node@20.14.5): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.12.4) + create-jest: 29.7.0(@types/node@20.14.5) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.12.4) + jest-config: 29.7.0(@types/node@20.14.5) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2786,12 +2944,11 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.12.4): + jest-config@29.7.0(@types/node@20.14.5): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.4 babel-jest: 29.7.0(@babel/core@7.23.9) chalk: 4.1.2 ci-info: 3.9.0 @@ -2811,6 +2968,8 @@ snapshots: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.14.5 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2839,7 +2998,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2849,7 +3008,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.12.4 + '@types/node': 20.14.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2888,11 +3047,11 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - dependencies: + optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {} @@ -2923,7 +3082,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2951,7 +3110,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -2990,14 +3149,14 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.5.4 + semver: 7.6.2 transitivePeerDependencies: - supports-color jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3016,7 +3175,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.4 + '@types/node': 20.14.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3025,17 +3184,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.12.4 + '@types/node': 20.14.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.12.4): + jest@29.7.0(@types/node@20.14.5): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.12.4) + jest-cli: 29.7.0(@types/node@20.14.5) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3071,7 +3230,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.19.4-alpha.18: {} + lemmy-js-client@0.19.5-alpha.1: {} leven@3.1.0: {} @@ -3098,13 +3257,9 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - make-dir@4.0.0: dependencies: - semver: 7.5.4 + semver: 7.6.2 make-error@1.3.6: {} @@ -3121,13 +3276,18 @@ snapshots: braces: 3.0.2 picomatch: 2.3.1 + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mimic-fn@2.1.0: {} minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - minimatch@9.0.3: + minimatch@9.0.4: dependencies: brace-expansion: 2.0.1 @@ -3217,7 +3377,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.2.5: {} + prettier@3.3.2: {} pretty-format@29.7.0: dependencies: @@ -3258,23 +3418,13 @@ snapshots: reusify@1.0.4: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 semver@6.3.1: {} - semver@7.5.4: - dependencies: - lru-cache: 6.0.0 - - semver@7.6.0: - dependencies: - lru-cache: 6.0.0 + semver@7.6.2: {} shebang-command@2.0.0: dependencies: @@ -3357,23 +3507,27 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@1.3.0(typescript@5.4.4): + ts-api-utils@1.3.0(typescript@5.4.5): dependencies: - typescript: 5.4.4 + typescript: 5.4.5 - ts-jest@29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.4.4): + ts-jest@29.1.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@20.14.5))(typescript@5.4.5): dependencies: - '@babel/core': 7.23.9 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.12.4) + jest: 29.7.0(@types/node@20.14.5) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.5.4 - typescript: 5.4.4 + semver: 7.6.2 + typescript: 5.4.5 yargs-parser: 21.1.1 + optionalDependencies: + '@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) tslib@2.6.2: {} @@ -3383,11 +3537,20 @@ snapshots: type-detect@4.0.8: {} - type-fest@0.20.2: {} - type-fest@0.21.3: {} - typescript@5.4.4: {} + typescript-eslint@7.13.0(eslint@9.5.0)(typescript@5.4.5): + dependencies: + '@typescript-eslint/eslint-plugin': 7.13.0(@typescript-eslint/parser@7.13.0(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.13.0(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.13.0(eslint@9.5.0)(typescript@5.4.5) + eslint: 9.5.0 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + typescript@5.4.5: {} undici-types@5.26.5: {} @@ -3432,8 +3595,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.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 31eb111c2..65c4827d9 100755 --- a/api_tests/prepare-drone-federation-test.sh +++ b/api_tests/prepare-drone-federation-test.sh @@ -15,7 +15,7 @@ export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queu # pictrs setup if [ ! -f "api_tests/pict-rs" ]; then - curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/pict-rs-linux-amd64" -o api_tests/pict-rs + curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o api_tests/pict-rs chmod +x api_tests/pict-rs fi ./api_tests/pict-rs \ diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index dfab4109c..8c3a23ab5 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -37,8 +37,9 @@ import { followCommunity, blockCommunity, delay, + saveUserSettings, } from "./shared"; -import { CommentView, CommunityView } from "lemmy-js-client"; +import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; let postOnAlphaRes: PostResponse; @@ -443,6 +444,59 @@ test("Reply to a comment from another instance, get notification", async () => { assertCommentFederation(alphaReply, replyRes.comment_view); }); +test("Bot reply notifications are filtered when bots are hidden", async () => { + const newAlphaBot = await registerUser(alpha, alphaUrl); + let form: SaveUserSettings = { + bot_account: true, + }; + await saveUserSettings(newAlphaBot, form); + + const alphaCommunity = ( + await resolveCommunity(alpha, "!main@lemmy-alpha:8541") + ).community; + + if (!alphaCommunity) { + throw "Missing alpha community"; + } + + await alpha.markAllAsRead(); + form = { + show_bot_accounts: false, + }; + await saveUserSettings(alpha, form); + const postOnAlphaRes = await createPost(alpha, alphaCommunity.community.id); + + // Bot reply to alpha's post + let commentRes = await createComment( + newAlphaBot, + postOnAlphaRes.post_view.post.id, + ); + expect(commentRes).toBeDefined(); + + let alphaUnreadCountRes = await getUnreadCount(alpha); + expect(alphaUnreadCountRes.replies).toBe(0); + + let alphaUnreadRepliesRes = await getReplies(alpha, true); + expect(alphaUnreadRepliesRes.replies.length).toBe(0); + + // This both restores the original state that may be expected by other tests + // implicitly and is used by the next steps to ensure replies are still + // returned when a user later decides to show bot accounts again. + form = { + show_bot_accounts: true, + }; + await saveUserSettings(alpha, form); + + alphaUnreadCountRes = await getUnreadCount(alpha); + expect(alphaUnreadCountRes.replies).toBe(1); + + alphaUnreadRepliesRes = await getReplies(alpha, true); + expect(alphaUnreadRepliesRes.replies.length).toBe(1); + expect(alphaUnreadRepliesRes.replies[0].comment.id).toBe( + commentRes.comment_view.comment.id, + ); +}); + test("Mention beta from alpha", async () => { if (!betaCommunity) throw Error("no community"); const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id); diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index 123982e85..fe9470d18 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -160,6 +160,7 @@ test("Purge post, linked image removed", async () => { upload.url, ); expect(post.post_view.post.url).toBe(upload.url); + expect(post.post_view.image_details).toBeDefined(); // purge post const purgeForm: PurgePost = { @@ -184,6 +185,9 @@ test("Images in remote image post are proxied if setting enabled", async () => { const post = postRes.post_view.post; expect(post).toBeDefined(); + // Make sure it fetched the image details + expect(postRes.post_view.image_details).toBeDefined(); + // remote image gets proxied after upload expect( post.thumbnail_url?.startsWith( diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index cb45274c6..fe17bd979 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -502,7 +502,7 @@ test("Enforce site ban federation for local user", async () => { } let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name); alphaUserHttp.setHeaders({ - Authorization: "Bearer " + newAlphaUserJwt.jwt ?? "", + Authorization: "Bearer " + newAlphaUserJwt.jwt, }); // alpha makes new post in beta community, it federates let postRes2 = await createPost(alphaUserHttp, betaCommunity!.community.id); diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 056f25538..2ae3d9e21 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -364,10 +364,13 @@ export async function getUnreadCount( return api.getUnreadCount(); } -export async function getReplies(api: LemmyHttp): Promise { +export async function getReplies( + api: LemmyHttp, + unread_only: boolean = false, +): Promise { let form: GetReplies = { sort: "New", - unread_only: false, + unread_only, }; return api.getReplies(form); } diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index f44f3cc0a..d008dcdc3 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -21,6 +21,7 @@ import { fetchFunction, alphaImage, unfollows, + saveUserSettingsBio, } from "./shared"; import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; @@ -186,10 +187,26 @@ test("Set a new avatar, old avatar is deleted", async () => { expect(upload2.url).toBeDefined(); let form2 = { - avatar: upload1.url, + avatar: upload2.url, }; await saveUserSettings(alpha, form2); // make sure only the new avatar is kept const listMediaRes2 = await alphaImage.listMedia(); expect(listMediaRes2.images.length).toBe(1); + + // Upload that same form2 avatar, make sure it isn't replaced / deleted + await saveUserSettings(alpha, form2); + // make sure only the new avatar is kept + const listMediaRes3 = await alphaImage.listMedia(); + expect(listMediaRes3.images.length).toBe(1); + + // Now try to save a user settings, with the icon missing, + // and make sure it doesn't clear the data, or delete the image + await saveUserSettingsBio(alpha); + let site = await getSite(alpha); + expect(site.my_user?.local_user_view.person.avatar).toBe(upload2.url); + + // make sure only the new avatar is kept + const listMediaRes4 = await alphaImage.listMedia(); + expect(listMediaRes4.images.length).toBe(1); }); diff --git a/cliff.toml b/cliff.toml index d8975a171..b5b8c3f16 100644 --- a/cliff.toml +++ b/cliff.toml @@ -26,6 +26,7 @@ body = """ {%- endif %} {%- endfor -%} +{%- if github -%} {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} {% raw %}\n{% endraw -%} ## New Contributors @@ -36,6 +37,7 @@ body = """ [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ {%- endif %} {%- endfor -%} +{%- endif -%} {% if version %} {% if previous.version %} @@ -70,6 +72,7 @@ commit_preprocessors = [ # remove issue numbers from commits { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, ] +commit_parsers = [{ field = "author.name", pattern = "renovate", skip = true }] # protect breaking changes from being skipped due to matching a skipping commit_parser protect_breaking_commits = false # filter out the commits that are not matched by commit parsers diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 846583c37..b98b15d62 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -33,7 +33,7 @@ anyhow = { workspace = true } tracing = { workspace = true } chrono = { workspace = true } url = { workspace = true } -wav = "1.0.1" +hound = "3.5.1" sitemap-rs = "0.2.1" totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] } actix-web-httpauth = "0.8.1" diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 93cf00415..877d9464f 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -43,7 +43,10 @@ pub async fn ban_from_community( &mut context.pool(), ) .await?; - is_valid_body_field(&data.reason, false)?; + + if let Some(reason) = &data.reason { + is_valid_body_field(reason, false)?; + } let community_user_ban_form = CommunityPersonBanForm { community_id: data.community_id, diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index c20e4ff9c..2b8e12d37 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -44,33 +44,38 @@ pub mod site; pub mod sitemap; /// Converts the captcha to a base64 encoded wav audio file -#[allow(deprecated)] pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult { let letters = captcha.as_wav(); // Decode each wav file, concatenate the samples let mut concat_samples: Vec = Vec::new(); - let mut any_header: Option = None; + let mut any_header: Option = None; for letter in letters { let mut cursor = Cursor::new(letter.unwrap_or_default()); - let (header, samples) = wav::read(&mut cursor)?; - any_header = Some(header); - if let Some(samples16) = samples.as_sixteen() { - concat_samples.extend(samples16); - } else { - Err(LemmyErrorType::CouldntCreateAudioCaptcha)? - } + let reader = hound::WavReader::new(&mut cursor)?; + any_header = Some(reader.spec()); + let samples16 = reader + .into_samples::() + .collect::, _>>() + .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; + concat_samples.extend(samples16); } // Encode the concatenated result as a wav file let mut output_buffer = Cursor::new(vec![]); if let Some(header) = any_header { - wav::write( - header, - &wav::BitDepth::Sixteen(concat_samples), - &mut output_buffer, - ) - .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; + let mut writer = hound::WavWriter::new(&mut output_buffer, header) + .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; + let mut writer16 = writer.get_i16_writer(concat_samples.len() as u32); + for sample in concat_samples { + writer16.write_sample(sample); + } + writer16 + .flush() + .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; + writer + .finalize() + .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; Ok(base64.encode(output_buffer.into_inner())) } else { diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index c31940fba..49cd6893a 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -31,7 +31,9 @@ pub async fn ban_from_site( // Make sure user is an admin is_admin(&local_user_view)?; - is_valid_body_field(&data.reason, false)?; + if let Some(reason) = &data.reason { + is_valid_body_field(reason, false)?; + } let expires = check_expire_time(data.expires)?; diff --git a/crates/api/src/local_user/notifications/unread_count.rs b/crates/api/src/local_user/notifications/unread_count.rs index 9d06f7c62..4c6c65263 100644 --- a/crates/api/src/local_user/notifications/unread_count.rs +++ b/crates/api/src/local_user/notifications/unread_count.rs @@ -11,9 +11,12 @@ pub async fn unread_count( ) -> LemmyResult> { let person_id = local_user_view.person.id; - let replies = CommentReplyView::get_unread_replies(&mut context.pool(), person_id).await?; + let replies = + CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).await?; - let mentions = PersonMentionView::get_unread_mentions(&mut context.pool(), person_id).await?; + let mentions = + PersonMentionView::get_unread_mentions(&mut context.pool(), &local_user_view.local_user) + .await?; let private_messages = PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?; diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index bdba817cc..193f9d269 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -21,7 +21,7 @@ use lemmy_db_schema::{ person::{Person, PersonUpdateForm}, }, traits::Crud, - utils::diesel_option_overwrite, + utils::{diesel_string_update, diesel_url_update}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ @@ -42,18 +42,24 @@ pub async fn save_user_settings( let slur_regex = local_site_to_slur_regex(&site_view.local_site); let url_blocklist = get_url_blocklist(&context).await?; - let bio = diesel_option_overwrite( - process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context).await?, + let bio = diesel_string_update( + process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context) + .await? + .as_deref(), ); - replace_image(&data.avatar, &local_user_view.person.avatar, &context).await?; - replace_image(&data.banner, &local_user_view.person.banner, &context).await?; - let avatar = proxy_image_link_opt_api(&data.avatar, &context).await?; - let banner = proxy_image_link_opt_api(&data.banner, &context).await?; - let display_name = diesel_option_overwrite(data.display_name.clone()); - let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone()); + let avatar = diesel_url_update(data.avatar.as_deref())?; + replace_image(&avatar, &local_user_view.person.avatar, &context).await?; + let avatar = proxy_image_link_opt_api(avatar, &context).await?; + + let banner = diesel_url_update(data.banner.as_deref())?; + replace_image(&banner, &local_user_view.person.banner, &context).await?; + let banner = proxy_image_link_opt_api(banner, &context).await?; + + let display_name = diesel_string_update(data.display_name.as_deref()); + let matrix_user_id = diesel_string_update(data.matrix_user_id.as_deref()); let email_deref = data.email.as_deref().map(str::to_lowercase); - let email = diesel_option_overwrite(email_deref.clone()); + let email = diesel_string_update(email_deref.as_deref()); if let Some(Some(email)) = &email { let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); diff --git a/crates/api/src/post/get_link_metadata.rs b/crates/api/src/post/get_link_metadata.rs index 17346790a..0669408aa 100644 --- a/crates/api/src/post/get_link_metadata.rs +++ b/crates/api/src/post/get_link_metadata.rs @@ -4,14 +4,19 @@ use lemmy_api_common::{ post::{GetSiteMetadata, GetSiteMetadataResponse}, request::fetch_link_metadata, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{ + error::{LemmyErrorExt, LemmyResult}, + LemmyErrorType, +}; +use url::Url; #[tracing::instrument(skip(context))] pub async fn get_link_metadata( data: Query, context: Data, ) -> LemmyResult> { - let metadata = fetch_link_metadata(&data.url, &context).await?; + let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?; + let metadata = fetch_link_metadata(&url, &context).await?; Ok(Json(GetSiteMetadataResponse { metadata })) } diff --git a/crates/api/src/site/registration_applications/approve.rs b/crates/api/src/site/registration_applications/approve.rs index 0fb55ffc8..823af54c4 100644 --- a/crates/api/src/site/registration_applications/approve.rs +++ b/crates/api/src/site/registration_applications/approve.rs @@ -10,7 +10,7 @@ use lemmy_db_schema::{ registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm}, }, traits::Crud, - utils::diesel_option_overwrite, + utils::diesel_string_update, }; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; @@ -26,7 +26,7 @@ pub async fn approve_registration_application( is_admin(&local_user_view)?; // Update the registration with reason, admin_id - let deny_reason = diesel_option_overwrite(data.deny_reason.clone()); + let deny_reason = diesel_string_update(data.deny_reason.as_deref()); let app_form = RegistrationApplicationUpdateForm { admin_id: Some(Some(local_user_view.person.id)), deny_reason, diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 160c581cd..6c17d4e6a 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -112,11 +112,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("Gerry9812".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 49327dac1..f42a468b5 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use url::Url; #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] @@ -20,8 +19,7 @@ use url::Url; pub struct CreatePost { pub name: String, pub community_id: CommunityId, - #[cfg_attr(feature = "full", ts(type = "string"))] - pub url: Option, + pub url: Option, /// An optional body for the post in markdown. pub body: Option, /// An optional alt_text, usable for image posts. @@ -30,9 +28,8 @@ pub struct CreatePost { pub honeypot: Option, pub nsfw: Option, pub language_id: Option, - #[cfg_attr(feature = "full", ts(type = "string"))] /// Instead of fetching a thumbnail, use a custom one. - pub custom_thumbnail: Option, + pub custom_thumbnail: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -82,6 +79,8 @@ pub struct GetPosts { pub liked_only: Option, pub disliked_only: Option, pub show_hidden: Option, + /// If true, then show the read posts (even if your user setting is to hide them) + pub show_read: Option, pub page_cursor: Option, } @@ -114,17 +113,15 @@ pub struct CreatePostLike { pub struct EditPost { pub post_id: PostId, pub name: Option, - #[cfg_attr(feature = "full", ts(type = "string"))] - pub url: Option, + pub url: Option, /// An optional body for the post in markdown. pub body: Option, /// An optional alt_text, usable for image posts. pub alt_text: Option, pub nsfw: Option, pub language_id: Option, - #[cfg_attr(feature = "full", ts(type = "string"))] /// Instead of fetching a thumbnail, use a custom one. - pub custom_thumbnail: Option, + pub custom_thumbnail: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] @@ -249,8 +246,7 @@ pub struct ListPostReportsResponse { #[cfg_attr(feature = "full", ts(export))] /// Get metadata for a given site. pub struct GetSiteMetadata { - #[cfg_attr(feature = "full", ts(type = "string"))] - pub url: Url, + pub url: String, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index c304bcba7..ddbb3dd0c 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -11,7 +11,7 @@ use encoding_rs::{Encoding, UTF_8}; use lemmy_db_schema::{ newtypes::DbUrl, source::{ - images::{LocalImage, LocalImageForm}, + images::{ImageDetailsForm, LocalImage, LocalImageForm}, local_site::LocalSite, post::{Post, PostUpdateForm}, }, @@ -209,6 +209,19 @@ pub struct PictrsFileDetails { pub created_at: DateTime, } +impl PictrsFileDetails { + /// Builds the image form. This should always use the thumbnail_url, + /// Because the post_view joins to it + pub fn build_image_details_form(&self, thumbnail_url: &Url) -> ImageDetailsForm { + ImageDetailsForm { + link: thumbnail_url.clone().into(), + width: self.width.into(), + height: self.height.into(), + content_type: self.content_type.clone(), + } + } +} + #[derive(Deserialize, Serialize, Debug)] struct PictrsPurgeResponse { msg: String, @@ -316,11 +329,52 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L let protocol_and_hostname = context.settings().get_protocol_and_hostname(); let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?; - LocalImage::create(&mut context.pool(), &form).await?; + // Also store the details for the image + let details_form = image.details.build_image_details_form(&thumbnail_url); + LocalImage::create(&mut context.pool(), &form, &details_form).await?; Ok(thumbnail_url) } +/// Fetches the image details for pictrs proxied images +/// +/// We don't need to check for image mode, as that's already been done +#[tracing::instrument(skip_all)] +pub async fn fetch_pictrs_proxied_image_details( + image_url: &Url, + context: &LemmyContext, +) -> LemmyResult { + let pictrs_url = context.settings().pictrs_config()?.url; + let encoded_image_url = encode(image_url.as_str()); + + // Pictrs needs you to fetch the proxied image before you can fetch the details + let proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}"); + + let res = context + .client() + .get(&proxy_url) + .timeout(REQWEST_TIMEOUT) + .send() + .await? + .status(); + if !res.is_success() { + Err(LemmyErrorType::NotAnImageType)? + } + + let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}"); + + let res = context + .client() + .get(&details_url) + .timeout(REQWEST_TIMEOUT) + .send() + .await? + .json() + .await?; + + Ok(res) +} + // TODO: get rid of this by reading content type from db #[tracing::instrument(skip_all)] async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> LemmyResult<()> { @@ -338,16 +392,19 @@ async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> Lemm } } -/// When adding a new avatar or similar image, delete the old one. +/// When adding a new avatar, banner or similar image, delete the old one. pub async fn replace_image( - new_image: &Option, + new_image: &Option>, old_image: &Option, context: &Data, ) -> LemmyResult<()> { - if new_image.is_some() { - // Ignore errors because image may be stored externally. - if let Some(avatar) = &old_image { - let image = LocalImage::delete_by_url(&mut context.pool(), avatar) + if let (Some(Some(new_image)), Some(old_image)) = (new_image, old_image) { + // Note: Oftentimes front ends will include the current image in the form. + // In this case, deleting `old_image` would also be deletion of `new_image`, + // so the deletion must be skipped for the image to be kept. + if new_image != old_image { + // Ignore errors because image may be stored externally. + let image = LocalImage::delete_by_url(&mut context.pool(), old_image) .await .ok(); if let Some(image) = image { diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index bad3b8180..97b12cc5b 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -1,6 +1,10 @@ use crate::{ context::LemmyContext, - request::{delete_image_from_pictrs, purge_image_from_pictrs}, + request::{ + delete_image_from_pictrs, + fetch_pictrs_proxied_image_details, + purge_image_from_pictrs, + }, site::{FederatedInstances, InstanceWithFederationState}, }; use chrono::{DateTime, Days, Local, TimeZone, Utc}; @@ -949,7 +953,18 @@ pub async fn process_markdown( if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages { let (text, links) = markdown_rewrite_image_links(text); - RemoteImage::create(&mut context.pool(), links).await?; + + // Create images and image detail rows + for link in links { + // Insert image details for the remote image + let details_res = fetch_pictrs_proxied_image_details(&link, context).await; + if let Ok(details) = details_res { + let proxied = + build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?; + let details_form = details.build_image_details_form(&proxied); + RemoteImage::create(&mut context.pool(), &details_form).await?; + } + } Ok(text) } else { Ok(text) @@ -984,8 +999,14 @@ async fn proxy_image_link_internal( Ok(link.into()) } else if image_mode == PictrsImageMode::ProxyAllImages { let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?; + // This should fail softly, since pictrs might not even be running + let details_res = fetch_pictrs_proxied_image_details(&link, context).await; + + if let Ok(details) = details_res { + let details_form = details.build_image_details_form(&proxied); + RemoteImage::create(&mut context.pool(), &details_form).await?; + }; - RemoteImage::create(&mut context.pool(), vec![link]).await?; Ok(proxied.into()) } else { Ok(link.into()) @@ -1004,26 +1025,25 @@ pub(crate) async fn proxy_image_link(link: Url, context: &LemmyContext) -> Lemmy } pub async fn proxy_image_link_opt_api( - link: &Option, + link: Option>, context: &LemmyContext, ) -> LemmyResult>> { - proxy_image_link_api(link, context).await.map(Some) + if let Some(Some(link)) = link { + proxy_image_link(link.into(), context) + .await + .map(Some) + .map(Some) + } else { + Ok(link) + } } pub async fn proxy_image_link_api( - link: &Option, + link: Option, context: &LemmyContext, ) -> LemmyResult> { - let link: Option = match link.as_ref().map(String::as_str) { - // An empty string is an erase - Some("") => None, - Some(str_url) => Url::parse(str_url) - .map(|u| Some(u.into())) - .with_lemmy_type(LemmyErrorType::InvalidUrl)?, - None => None, - }; - if let Some(l) = link { - proxy_image_link(l.into(), context).await.map(Some) + if let Some(link) = link { + proxy_image_link(link.into(), context).await.map(Some) } else { Ok(link) } @@ -1124,35 +1144,13 @@ mod tests { "https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png", proxied.as_str() ); + + // This fails, because the details can't be fetched without pictrs running, + // And a remote image won't be inserted. assert!( RemoteImage::validate(&mut context.pool(), remote_image.into()) - .await - .is_ok() - ); - } - - #[tokio::test] - #[serial] - async fn test_diesel_option_overwrite_to_url() { - let context = LemmyContext::init_test_context().await; - - assert!(matches!( - proxy_image_link_api(&None, &context).await, - Ok(None) - )); - assert!(matches!( - proxy_image_link_opt_api(&Some(String::new()), &context).await, - Ok(Some(None)) - )); - assert!( - proxy_image_link_opt_api(&Some("invalid_url".to_string()), &context) .await .is_err() ); - let example_url = "https://lemmy-alpha/image.png"; - assert!(matches!( - proxy_image_link_opt_api(&Some(example_url.to_string()), &context).await, - Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into() - )); } } diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 2efd46964..636f83392 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -47,7 +47,7 @@ pub async fn create_comment( let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; - is_valid_body_field(&Some(content.clone()), false)?; + is_valid_body_field(&content, false)?; // Check for a community ban let post_id = data.post_id; diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 695ededfe..4c8cf9436 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -63,7 +63,9 @@ pub async fn update_comment( let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown_opt(&data.content, &slur_regex, &url_blocklist, &context).await?; - is_valid_body_field(&content, false)?; + if let Some(content) = &content { + is_valid_body_field(content, false)?; + } let comment_id = data.comment_id; let form = CommentUpdateForm { diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index b0b6bea0e..4289b7d24 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -30,6 +30,7 @@ use lemmy_db_schema::{ }, }, traits::{ApubActor, Crud, Followable, Joinable}, + utils::diesel_url_create, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ @@ -61,11 +62,18 @@ pub async fn create_community( check_slurs(&data.title, &slur_regex)?; let description = process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?; - let icon = proxy_image_link_api(&data.icon, &context).await?; - let banner = proxy_image_link_api(&data.banner, &context).await?; + + let icon = diesel_url_create(data.icon.as_deref())?; + let icon = proxy_image_link_api(icon, &context).await?; + + let banner = diesel_url_create(data.banner.as_deref())?; + let banner = proxy_image_link_api(banner, &context).await?; is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?; - is_valid_body_field(&data.description, false)?; + + if let Some(desc) = &data.description { + is_valid_body_field(desc, false)?; + } // Double check for duplicate community actor_ids let community_actor_id = generate_local_apub_endpoint( diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index 33c6a47dd..6190a0ca7 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -21,7 +21,7 @@ use lemmy_db_schema::{ local_site::LocalSite, }, traits::Crud, - utils::{diesel_option_overwrite, naive_now}, + utils::{diesel_string_update, diesel_url_update, naive_now}, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ @@ -40,18 +40,28 @@ pub async fn update_community( let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; check_slurs_opt(&data.title, &slur_regex)?; - let description = - process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?; - is_valid_body_field(&data.description, false)?; + + let description = diesel_string_update( + process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context) + .await? + .as_deref(), + ); + + if let Some(Some(desc)) = &description { + is_valid_body_field(desc, false)?; + } + let old_community = Community::read(&mut context.pool(), data.community_id) .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?; - replace_image(&data.icon, &old_community.icon, &context).await?; - replace_image(&data.banner, &old_community.banner, &context).await?; - let description = diesel_option_overwrite(description); - let icon = proxy_image_link_opt_api(&data.icon, &context).await?; - let banner = proxy_image_link_opt_api(&data.banner, &context).await?; + let icon = diesel_url_update(data.icon.as_deref())?; + replace_image(&icon, &old_community.icon, &context).await?; + let icon = proxy_image_link_opt_api(icon, &context).await?; + + let banner = diesel_url_update(data.banner.as_deref())?; + replace_image(&banner, &old_community.banner, &context).await?; + let banner = proxy_image_link_opt_api(banner, &context).await?; // Verify its a mod (only mods can edit it) check_community_mod_action( diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 21a989d3f..0b0fad5dc 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -26,6 +26,7 @@ use lemmy_db_schema::{ post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm}, }, traits::{Crud, Likeable}, + utils::diesel_url_create, CommunityVisibility, }; use lemmy_db_views::structs::LocalUserView; @@ -37,7 +38,6 @@ use lemmy_utils::{ slurs::check_slurs, validation::{ check_url_scheme, - clean_url_params, is_url_blocked, is_valid_alt_text_field, is_valid_body_field, @@ -64,16 +64,27 @@ pub async fn create_post( let url_blocklist = get_url_blocklist(&context).await?; let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?; - let data_url = data.url.as_ref(); - let url = data_url.map(clean_url_params); // TODO no good way to handle a "clear" - let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params); + let url = diesel_url_create(data.url.as_deref())?; + let custom_thumbnail = diesel_url_create(data.custom_thumbnail.as_deref())?; is_valid_post_title(&data.name)?; - is_valid_body_field(&body, true)?; - is_valid_alt_text_field(&data.alt_text)?; - is_url_blocked(&url, &url_blocklist)?; - check_url_scheme(&url)?; - check_url_scheme(&custom_thumbnail)?; + + if let Some(url) = &url { + is_url_blocked(url, &url_blocklist)?; + check_url_scheme(url)?; + } + + if let Some(custom_thumbnail) = &custom_thumbnail { + check_url_scheme(custom_thumbnail)?; + } + + if let Some(alt_text) = &data.alt_text { + is_valid_alt_text_field(alt_text)?; + } + + if let Some(body) = &body { + is_valid_body_field(body, true)?; + } check_community_user_action( &local_user_view.person, @@ -156,7 +167,7 @@ pub async fn create_post( generate_post_link_metadata( updated_post.clone(), - custom_thumbnail, + custom_thumbnail.map(Into::into), |post| Some(SendActivityData::CreatePost(post)), Some(local_site), context.reset_request_count(), diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 506b97299..60b5609a9 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -83,11 +83,13 @@ pub async fn get_post( .ok_or(LemmyErrorType::CouldntFindCommunity)?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; + let local_user = local_user_view.as_ref().map(|u| &u.local_user); // Fetch the cross_posts let cross_posts = if let Some(url) = &post_view.post.url { let mut x_posts = PostQuery { url_search: Some(url.inner().as_str().into()), + local_user, ..Default::default() } .list(&local_site.site, &mut context.pool()) diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index 4b4bd9845..9e665aed6 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -20,16 +20,15 @@ use lemmy_db_schema::{ post::{Post, PostUpdateForm}, }, traits::Crud, - utils::{diesel_option_overwrite, naive_now}, + utils::{diesel_string_update, diesel_url_update, naive_now}, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ - slurs::check_slurs_opt, + slurs::check_slurs, validation::{ check_url_scheme, - clean_url_params, is_url_blocked, is_valid_alt_text_field, is_valid_body_field, @@ -47,26 +46,43 @@ pub async fn update_post( ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; - // TODO No good way to handle a clear. - // Issue link: https://github.com/LemmyNet/lemmy/issues/2287 - let url = data.url.as_ref().map(clean_url_params); - let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params); + 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); - check_slurs_opt(&data.name, &slur_regex)?; - let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?; + + let body = diesel_string_update( + process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context) + .await? + .as_deref(), + ); + + let alt_text = diesel_string_update(data.alt_text.as_deref()); if let Some(name) = &data.name { is_valid_post_title(name)?; + check_slurs(name, &slur_regex)?; } - is_valid_body_field(&body, true)?; - is_valid_alt_text_field(&data.alt_text)?; - is_url_blocked(&url, &url_blocklist)?; - check_url_scheme(&url)?; - check_url_scheme(&custom_thumbnail)?; + if let Some(Some(body)) = &body { + is_valid_body_field(body, true)?; + } + + if let Some(Some(alt_text)) = &alt_text { + is_valid_alt_text_field(alt_text)?; + } + + if let Some(Some(url)) = &url { + is_url_blocked(url, &url_blocklist)?; + check_url_scheme(url)?; + } + + if let Some(Some(custom_thumbnail)) = &custom_thumbnail { + check_url_scheme(custom_thumbnail)?; + } let post_id = data.post_id; let orig_post = Post::read(&mut context.pool(), post_id) @@ -95,9 +111,9 @@ pub async fn update_post( let post_form = PostUpdateForm { name: data.name.clone(), - url: Some(url.map(Into::into)), - body: diesel_option_overwrite(body), - alt_text: diesel_option_overwrite(data.alt_text.clone()), + url, + body, + alt_text, nsfw: data.nsfw, language_id: data.language_id, updated: Some(Some(naive_now())), @@ -111,7 +127,7 @@ pub async fn update_post( generate_post_link_metadata( updated_post.clone(), - custom_thumbnail, + custom_thumbnail.flatten().map(Into::into), |post| Some(SendActivityData::UpdatePost(post)), Some(local_site), context.reset_request_count(), diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index e977a6c86..0381d196c 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -39,7 +39,7 @@ pub async fn create_private_message( let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; - is_valid_body_field(&Some(content.clone()), false)?; + is_valid_body_field(&content, false)?; check_person_block( local_user_view.person.id, diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 2842fea65..364d5c2e3 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -41,7 +41,7 @@ pub async fn update_private_message( let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; - is_valid_body_field(&Some(content.clone()), false)?; + is_valid_body_field(&content, false)?; let private_message_id = data.private_message_id; PrivateMessage::update( diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 466c7ff1d..6b1909966 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -11,7 +11,7 @@ use lemmy_api_common::{ local_site_rate_limit_to_rate_limit_config, local_site_to_slur_regex, process_markdown_opt, - proxy_image_link_opt_api, + proxy_image_link_api, }, }; use lemmy_db_schema::{ @@ -23,7 +23,7 @@ use lemmy_db_schema::{ tagline::Tagline, }, traits::Crud, - utils::{diesel_option_overwrite, naive_now}, + utils::{diesel_string_update, diesel_url_create, naive_now}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ @@ -61,21 +61,25 @@ pub async fn create_site( let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?; - let icon = proxy_image_link_opt_api(&data.icon, &context).await?; - let banner = proxy_image_link_opt_api(&data.banner, &context).await?; + + let icon = diesel_url_create(data.icon.as_deref())?; + let icon = proxy_image_link_api(icon, &context).await?; + + let banner = diesel_url_create(data.banner.as_deref())?; + let banner = proxy_image_link_api(banner, &context).await?; let site_form = SiteUpdateForm { name: Some(data.name.clone()), - sidebar: diesel_option_overwrite(sidebar), - description: diesel_option_overwrite(data.description.clone()), - icon, - banner, + sidebar: diesel_string_update(sidebar.as_deref()), + description: diesel_string_update(data.description.as_deref()), + icon: Some(icon), + banner: Some(banner), actor_id: Some(actor_id), last_refreshed_at: Some(naive_now()), inbox_url, private_key: Some(Some(keypair.private_key)), public_key: Some(keypair.public_key), - content_warning: diesel_option_overwrite(data.content_warning.clone()), + content_warning: diesel_string_update(data.content_warning.as_deref()), ..Default::default() }; @@ -91,16 +95,16 @@ pub async fn create_site( enable_nsfw: data.enable_nsfw, community_creation_admin_only: data.community_creation_admin_only, require_email_verification: data.require_email_verification, - application_question: diesel_option_overwrite(data.application_question.clone()), + application_question: diesel_string_update(data.application_question.as_deref()), private_instance: data.private_instance, default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, default_sort_type: data.default_sort_type, - legal_information: diesel_option_overwrite(data.legal_information.clone()), + legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, updated: Some(Some(naive_now())), - slur_filter_regex: diesel_option_overwrite(data.slur_filter_regex.clone()), + slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()), actor_name_max_length: data.actor_name_max_length, federation_enabled: data.federation_enabled, captcha_enabled: data.captcha_enabled, @@ -179,7 +183,9 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> )?; // Ensure that the sidebar has fewer than the max num characters... - is_valid_body_field(&create_site.sidebar, false)?; + if let Some(body) = &create_site.sidebar { + is_valid_body_field(body, false)?; + } application_question_check( &local_site.application_question, diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 7efa9b568..f6377038d 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ tagline::Tagline, }, traits::Crud, - utils::{diesel_option_overwrite, naive_now}, + utils::{diesel_string_update, diesel_url_update, naive_now}, RegistrationMode, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; @@ -67,22 +67,29 @@ pub async fn update_site( SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?; } - replace_image(&data.icon, &site.icon, &context).await?; - replace_image(&data.banner, &site.banner, &context).await?; - let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; - let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?; - let icon = proxy_image_link_opt_api(&data.icon, &context).await?; - let banner = proxy_image_link_opt_api(&data.banner, &context).await?; + let sidebar = diesel_string_update( + process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context) + .await? + .as_deref(), + ); + + let icon = diesel_url_update(data.icon.as_deref())?; + replace_image(&icon, &site.icon, &context).await?; + let icon = proxy_image_link_opt_api(icon, &context).await?; + + let banner = diesel_url_update(data.banner.as_deref())?; + replace_image(&banner, &site.banner, &context).await?; + let banner = proxy_image_link_opt_api(banner, &context).await?; let site_form = SiteUpdateForm { name: data.name.clone(), - sidebar: diesel_option_overwrite(sidebar), - description: diesel_option_overwrite(data.description.clone()), + sidebar, + description: diesel_string_update(data.description.as_deref()), icon, banner, - content_warning: diesel_option_overwrite(data.content_warning.clone()), + content_warning: diesel_string_update(data.content_warning.as_deref()), updated: Some(Some(naive_now())), ..Default::default() }; @@ -99,16 +106,16 @@ pub async fn update_site( enable_nsfw: data.enable_nsfw, community_creation_admin_only: data.community_creation_admin_only, require_email_verification: data.require_email_verification, - application_question: diesel_option_overwrite(data.application_question.clone()), + application_question: diesel_string_update(data.application_question.as_deref()), private_instance: data.private_instance, default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, default_sort_type: data.default_sort_type, - legal_information: diesel_option_overwrite(data.legal_information.clone()), + legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, updated: Some(Some(naive_now())), - slur_filter_regex: diesel_option_overwrite(data.slur_filter_regex.clone()), + slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()), actor_name_max_length: data.actor_name_max_length, federation_enabled: data.federation_enabled, captcha_enabled: data.captcha_enabled, @@ -229,7 +236,9 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm )?; // Ensure that the sidebar has fewer than the max num characters... - is_valid_body_field(&edit_site.sidebar, false)?; + if let Some(body) = &edit_site.sidebar { + is_valid_body_field(body, false)?; + } application_question_check( &local_site.application_question, diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index bf064fb2e..c84bd0a50 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -112,15 +112,17 @@ pub async fn register( // We have to create both a person, and local_user // Register the new person - let person_form = PersonInsertForm::builder() - .name(data.username.clone()) - .actor_id(Some(actor_id.clone())) - .private_key(Some(actor_keypair.private_key)) - .public_key(actor_keypair.public_key) - .inbox_url(Some(generate_inbox_url(&actor_id)?)) - .shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?)) - .instance_id(site_view.site.instance_id) - .build(); + let person_form = PersonInsertForm { + actor_id: Some(actor_id.clone()), + inbox_url: Some(generate_inbox_url(&actor_id)?), + shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + private_key: Some(actor_keypair.private_key), + ..PersonInsertForm::new( + data.username.clone(), + actor_keypair.public_key, + site_view.site.instance_id, + ) + }; // insert the person let inserted_person = Person::create(&mut context.pool(), &person_form) diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 25d197007..12d18110e 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -37,11 +37,11 @@ pub async fn list_comments( }; let sort = data.sort; let max_depth = data.max_depth; - let saved_only = data.saved_only.unwrap_or_default(); + let saved_only = data.saved_only; - let liked_only = data.liked_only.unwrap_or_default(); - let disliked_only = data.disliked_only.unwrap_or_default(); - if liked_only && disliked_only { + let liked_only = data.liked_only; + let disliked_only = data.disliked_only; + if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() { return Err(LemmyError::from(LemmyErrorType::ContradictingFilters)); } @@ -70,6 +70,8 @@ pub async fn list_comments( let parent_path_cloned = parent_path.clone(); let post_id = data.post_id; + let local_user = local_user_view.as_ref().map(|l| &l.local_user); + let comments = CommentQuery { listing_type, sort, @@ -80,7 +82,7 @@ pub async fn list_comments( community_id, parent_path: parent_path_cloned, post_id, - local_user: local_user_view.as_ref(), + local_user, page, limit, ..Default::default() diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index ec5412de8..c00c87f4c 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -40,26 +40,27 @@ pub async fn list_posts( } else { data.community_id }; - let saved_only = data.saved_only.unwrap_or_default(); - let show_hidden = data.show_hidden.unwrap_or_default(); + let saved_only = data.saved_only; + let show_hidden = data.show_hidden; + let show_read = data.show_read; - let liked_only = data.liked_only.unwrap_or_default(); - let disliked_only = data.disliked_only.unwrap_or_default(); - if liked_only && disliked_only { + let liked_only = data.liked_only; + let disliked_only = data.disliked_only; + if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() { return Err(LemmyError::from(LemmyErrorType::ContradictingFilters)); } - let local_user_ref = local_user_view.as_ref().map(|u| &u.local_user); + let local_user = local_user_view.as_ref().map(|u| &u.local_user); let listing_type = Some(listing_type_with_default( data.type_, - local_user_ref, + local_user, &local_site.local_site, community_id, )); let sort = Some(sort_type_with_default( data.sort, - local_user_ref, + local_user, &local_site.local_site, )); @@ -71,7 +72,7 @@ pub async fn list_posts( }; let posts = PostQuery { - local_user: local_user_view.as_ref(), + local_user, listing_type, sort, community_id, @@ -82,6 +83,7 @@ pub async fn list_posts( page_after, limit, show_hidden, + show_read, ..Default::default() } .list(&local_site.site, &mut context.pool()) diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index 0d65ab4f7..149e06a17 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -55,20 +55,22 @@ pub async fn read_person( let sort = data.sort; let page = data.page; let limit = data.limit; - let saved_only = data.saved_only.unwrap_or_default(); + let saved_only = data.saved_only; let community_id = data.community_id; // If its saved only, you don't care what creator it was // Or, if its not saved, then you only want it for that specific creator - let creator_id = if !saved_only { + let creator_id = if !saved_only.unwrap_or_default() { Some(person_details_id) } else { None }; + let local_user = local_user_view.as_ref().map(|l| &l.local_user); + let posts = PostQuery { sort, saved_only, - local_user: local_user_view.as_ref(), + local_user, community_id, page, limit, @@ -79,7 +81,7 @@ pub async fn read_person( .await?; let comments = CommentQuery { - local_user: local_user_view.as_ref(), + local_user, sort: sort.map(post_to_comment_sort_type), saved_only, community_id, diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index f3cd36faf..a048b64a7 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -55,7 +55,7 @@ pub async fn search( data.community_id }; let creator_id = data.creator_id; - let local_user = local_user_view.as_ref().map(|luv| &luv.local_user); + let local_user = local_user_view.as_ref().map(|l| &l.local_user); match search_type { SearchType::Posts => { @@ -64,7 +64,7 @@ pub async fn search( listing_type: (listing_type), community_id: (community_id), creator_id: (creator_id), - local_user: (local_user_view.as_ref()), + local_user, search_term: (Some(q)), page: (page), limit: (limit), @@ -80,7 +80,7 @@ pub async fn search( search_term: (Some(q)), community_id: (community_id), creator_id: (creator_id), - local_user: (local_user_view.as_ref()), + local_user, page: (page), limit: (limit), ..Default::default() @@ -125,7 +125,7 @@ pub async fn search( listing_type: (listing_type), community_id: (community_id), creator_id: (creator_id), - local_user: (local_user_view.as_ref()), + local_user, search_term: (Some(q)), page: (page), limit: (limit), @@ -142,7 +142,7 @@ pub async fn search( search_term: (Some(q)), community_id: (community_id), creator_id: (creator_id), - local_user: (local_user_view.as_ref()), + local_user, page: (page), limit: (limit), ..Default::default() @@ -192,6 +192,7 @@ pub async fn search( community_id: (community_id), creator_id: (creator_id), url_search: (Some(q)), + local_user, page: (page), limit: (limit), ..Default::default() diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 558551632..9f2cb58c5 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -338,13 +338,11 @@ mod tests { context: &Data, ) -> LemmyResult { let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?; - let person_form = PersonInsertForm::builder() - .name(name.clone()) - .display_name(Some(name.clone())) - .bio(bio) - .public_key("asd".to_string()) - .instance_id(instance.id) - .build(); + let person_form = PersonInsertForm { + display_name: Some(name.clone()), + bio, + ..PersonInsertForm::test_form(instance.id, &name) + }; let person = Person::create(&mut context.pool(), &person_form).await?; let user_form = LocalUserInsertForm::builder() diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs index 02b912f44..8e5419c7e 100644 --- a/crates/apub/src/collections/community_moderators.rs +++ b/crates/apub/src/collections/community_moderators.rs @@ -129,11 +129,7 @@ mod tests { let inserted_instance = Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?; - let old_mod = PersonInsertForm::builder() - .name("holly".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let old_mod = PersonInsertForm::test_form(inserted_instance.id, "holly"); let old_mod = Person::create(&mut context.pool(), &old_mod).await?; let community_moderator_form = CommunityModeratorForm { diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 59f4920d3..7e4254840 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -219,7 +219,10 @@ impl Object for ApubPost { } else { None }; - check_url_scheme(&url)?; + + if let Some(url) = &url { + check_url_scheme(url)?; + } let alt_text = first_attachment.cloned().and_then(Attachment::alt_text); diff --git a/crates/db_perf/src/main.rs b/crates/db_perf/src/main.rs index 9092d7514..8e03a0a1d 100644 --- a/crates/db_perf/src/main.rs +++ b/crates/db_perf/src/main.rs @@ -72,11 +72,7 @@ async fn try_main() -> LemmyResult<()> { println!("🫃 creating {} people", args.people); let mut person_ids = vec![]; for i in 0..args.people.get() { - let form = PersonInsertForm::builder() - .name(format!("p{i}")) - .public_key("pubkey".to_owned()) - .instance_id(instance.id) - .build(); + let form = PersonInsertForm::test_form(instance.id, &format!("p{i}")); person_ids.push(Person::create(&mut conn.into(), &form).await?.id); } diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index 9cd04cae3..c153e51c3 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -81,6 +81,7 @@ uuid = { workspace = true, features = ["v4"] } i-love-jesus = { workspace = true, optional = true } anyhow = { workspace = true } moka.workspace = true +derive-new.workspace = true [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index fa5b01018..87866e89c 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -5,12 +5,17 @@ -- (even if only other columns are updated) because triggers can run after the deletion of referenced rows and -- before the automatic deletion of the row that references it. This is not a problem for insert or delete. -- --- After a row update begins, a concurrent update on the same row can't begin until the whole --- transaction that contains the first update is finished. To reduce this locking, statements in --- triggers should be ordered based on the likelihood of concurrent writers. For example, updating --- site_aggregates should be done last because the same row is updated for all local stuff. If --- it were not last, then the locking period for concurrent writers would extend to include the --- time consumed by statements that come after. +-- Triggers that update multiple tables should use this order: person_aggregates, comment_aggregates, +-- post_aggregates, 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 +-- updates. Long locks are worse on rows that have more concurrent transactions trying to update them. The +-- listed order starts with tables that are less likely to have such rows. +-- https://www.postgresql.org/docs/16/transaction-iso.html#XACT-READ-COMMITTED +-- * Using the same order in every trigger matters because a deadlock is possible if multiple transactions +-- update the same rows in a different order. +-- https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS -- -- -- Create triggers for both post and comments @@ -481,7 +486,7 @@ BEGIN INNER JOIN old_post ON old_post.id = new_post.id AND (old_post.featured_community, old_post.featured_local) != (new_post.featured_community, - old_post.featured_local) + new_post.featured_local) WHERE post_aggregates.post_id = new_post.id; RETURN NULL; diff --git a/crates/db_schema/src/aggregates/comment_aggregates.rs b/crates/db_schema/src/aggregates/comment_aggregates.rs index 915d17b1d..92b24beb5 100644 --- a/crates/db_schema/src/aggregates/comment_aggregates.rs +++ b/crates/db_schema/src/aggregates/comment_aggregates.rs @@ -64,19 +64,11 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("thommy_comment_agg".into()) - .public_key("pubkey".into()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_comment_agg"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let another_person = PersonInsertForm::builder() - .name("jerry_comment_agg".into()) - .public_key("pubkey".into()) - .instance_id(inserted_instance.id) - .build(); + let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_comment_agg"); let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); diff --git a/crates/db_schema/src/aggregates/community_aggregates.rs b/crates/db_schema/src/aggregates/community_aggregates.rs index 0cf63809d..fe9de62bb 100644 --- a/crates/db_schema/src/aggregates/community_aggregates.rs +++ b/crates/db_schema/src/aggregates/community_aggregates.rs @@ -65,19 +65,11 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("thommy_community_agg".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let another_person = PersonInsertForm::builder() - .name("jerry_community_agg".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index 03295173f..a8767895c 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -49,19 +49,11 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("thommy_user_agg".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_user_agg"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let another_person = PersonInsertForm::builder() - .name("jerry_user_agg".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_user_agg"); let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); diff --git a/crates/db_schema/src/aggregates/post_aggregates.rs b/crates/db_schema/src/aggregates/post_aggregates.rs index cb8227795..eba3a02a3 100644 --- a/crates/db_schema/src/aggregates/post_aggregates.rs +++ b/crates/db_schema/src/aggregates/post_aggregates.rs @@ -83,19 +83,11 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("thommy_community_agg".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let another_person = PersonInsertForm::builder() - .name("jerry_community_agg".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); @@ -229,11 +221,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("thommy_community_agg".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index 268a37aac..ee9a1be9c 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -42,11 +42,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("thommy_site_agg".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_site_agg"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index 682be2ed0..8483d6c20 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -531,11 +531,7 @@ mod tests { let (site, instance) = create_test_site(pool).await; - let person_form = PersonInsertForm::builder() - .name("my test person".to_string()) - .public_key("pubkey".to_string()) - .instance_id(instance.id) - .build(); + let person_form = PersonInsertForm::test_form(instance.id, "my test person"); let person = Person::create(pool, &person_form).await.unwrap(); let local_user_form = LocalUserInsertForm::builder() .person_id(person.id) @@ -647,11 +643,7 @@ mod tests { .await .unwrap(); - let person_form = PersonInsertForm::builder() - .name("my test person".to_string()) - .public_key("pubkey".to_string()) - .instance_id(instance.id) - .build(); + let person_form = PersonInsertForm::test_form(instance.id, "my test person"); let person = Person::create(pool, &person_form).await.unwrap(); let local_user_form = LocalUserInsertForm::builder() .person_id(person.id) diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index eff7da26f..0ffd53f86 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -118,8 +118,9 @@ impl Crud for Comment { type IdType = CommentId; /// This is unimplemented, use [[Comment::create]] - async fn create(_pool: &mut DbPool<'_>, _comment_form: &Self::InsertForm) -> Result { - unimplemented!(); + async fn create(pool: &mut DbPool<'_>, comment_form: &Self::InsertForm) -> Result { + debug_assert!(false); + Comment::create(pool, comment_form, None).await } async fn update( @@ -233,11 +234,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("terry".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "terry"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 02b65e6fc..6cd90cc66 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -434,11 +434,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("bobbee".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "bobbee"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); diff --git a/crates/db_schema/src/impls/images.rs b/crates/db_schema/src/impls/images.rs index 9589aeee3..547bfc4e2 100644 --- a/crates/db_schema/src/impls/images.rs +++ b/crates/db_schema/src/impls/images.rs @@ -1,7 +1,14 @@ use crate::{ newtypes::DbUrl, - schema::{local_image, remote_image}, - source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm}, + schema::{image_details, local_image, remote_image}, + source::images::{ + ImageDetails, + ImageDetailsForm, + LocalImage, + LocalImageForm, + RemoteImage, + RemoteImageForm, + }, utils::{get_conn, DbPool}, }; use diesel::{ @@ -13,15 +20,29 @@ use diesel::{ NotFound, QueryDsl, }; -use diesel_async::RunQueryDsl; -use url::Url; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; impl LocalImage { - pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result { + pub async fn create( + pool: &mut DbPool<'_>, + form: &LocalImageForm, + image_details_form: &ImageDetailsForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(local_image::table) - .values(form) - .get_result::(conn) + conn + .build_transaction() + .run(|conn| { + Box::pin(async move { + let local_insert = insert_into(local_image::table) + .values(form) + .get_result::(conn) + .await; + + ImageDetails::create(conn, image_details_form).await?; + + local_insert + }) as _ + }) .await } @@ -39,16 +60,26 @@ impl LocalImage { } impl RemoteImage { - pub async fn create(pool: &mut DbPool<'_>, links: Vec) -> Result { + pub async fn create(pool: &mut DbPool<'_>, form: &ImageDetailsForm) -> Result { let conn = &mut get_conn(pool).await?; - let forms = links - .into_iter() - .map(|url| RemoteImageForm { link: url.into() }) - .collect::>(); - insert_into(remote_image::table) - .values(forms) - .on_conflict_do_nothing() - .execute(conn) + conn + .build_transaction() + .run(|conn| { + Box::pin(async move { + let remote_image_form = RemoteImageForm { + link: form.link.clone(), + }; + let remote_insert = insert_into(remote_image::table) + .values(remote_image_form) + .on_conflict_do_nothing() + .execute(conn) + .await; + + ImageDetails::create(conn, form).await?; + + remote_insert + }) as _ + }) .await } @@ -67,3 +98,16 @@ impl RemoteImage { } } } + +impl ImageDetails { + pub(crate) async fn create( + conn: &mut AsyncPgConnection, + form: &ImageDetailsForm, + ) -> Result { + insert_into(image_details::table) + .values(form) + .on_conflict_do_nothing() + .execute(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 62fc418d0..9b59e07ba 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -5,6 +5,7 @@ use crate::{ actor_language::LocalUserLanguage, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeInsertForm}, + site::Site, }, utils::{ functions::{coalesce, lower}, @@ -216,6 +217,44 @@ impl LocalUser { } } +/// Adds some helper functions for an optional LocalUser +pub trait LocalUserOptionHelper { + fn person_id(&self) -> Option; + fn local_user_id(&self) -> Option; + fn show_bot_accounts(&self) -> bool; + fn show_read_posts(&self) -> bool; + fn is_admin(&self) -> bool; + fn show_nsfw(&self, site: &Site) -> bool; +} + +impl LocalUserOptionHelper for Option<&LocalUser> { + fn person_id(&self) -> Option { + self.map(|l| l.person_id) + } + + fn local_user_id(&self) -> Option { + self.map(|l| l.id) + } + + fn show_bot_accounts(&self) -> bool { + self.map(|l| l.show_bot_accounts).unwrap_or(true) + } + + fn show_read_posts(&self) -> bool { + self.map(|l| l.show_read_posts).unwrap_or(true) + } + + fn is_admin(&self) -> bool { + self.map(|l| l.admin).unwrap_or(false) + } + + fn show_nsfw(&self, site: &Site) -> bool { + self + .map(|l| l.show_nsfw) + .unwrap_or(site.content_warning.is_some()) + } +} + impl LocalUserInsertForm { pub fn test_form(person_id: PersonId) -> Self { Self::builder() diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/moderator.rs index 125bbcd51..c10d818f8 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/moderator.rs @@ -513,19 +513,11 @@ mod tests { .await .unwrap(); - let new_mod = PersonInsertForm::builder() - .name("the mod".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_mod = PersonInsertForm::test_form(inserted_instance.id, "the mod"); let inserted_mod = Person::create(pool, &new_mod).await.unwrap(); - let new_person = PersonInsertForm::builder() - .name("jim2".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "jim2"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index c92cb0867..0b1351af1 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -70,11 +70,7 @@ mod tests { // Setup let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let new_person = PersonInsertForm::builder() - .name("thommy prw".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy prw"); let inserted_person = Person::create(pool, &new_person).await?; let new_local_user = LocalUserInsertForm::builder() .person_id(inserted_person.id) diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index 0255785d6..f318a503a 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -12,7 +12,14 @@ use crate::{ traits::{ApubActor, Crud, Followable}, utils::{functions::lower, get_conn, naive_now, DbPool}, }; -use diesel::{dsl::insert_into, result::Error, CombineDsl, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{ + dsl::{insert_into, not}, + result::Error, + CombineDsl, + ExpressionMethods, + JoinOnDsl, + QueryDsl, +}; use diesel_async::RunQueryDsl; #[async_trait] @@ -100,6 +107,8 @@ impl Person { .inner_join(post::table) .inner_join(community::table.on(post::community_id.eq(community::id))) .filter(community::local.eq(true)) + .filter(not(community::deleted)) + .filter(not(community::removed)) .filter(comment::creator_id.eq(for_creator_id)) .select(community::id) .union( @@ -116,11 +125,7 @@ impl Person { impl PersonInsertForm { pub fn test_form(instance_id: InstanceId, name: &str) -> Self { - Self::builder() - .name(name.to_owned()) - .public_key("pubkey".to_string()) - .instance_id(instance_id) - .build() + Self::new(name.to_owned(), "pubkey".to_string(), instance_id) } } @@ -240,11 +245,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("holly".into()) - .public_key("nada".to_owned()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "holly"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); @@ -263,7 +264,7 @@ mod tests { local: true, bot_account: false, private_key: None, - public_key: "nada".to_owned(), + public_key: "pubkey".to_owned(), last_refreshed_at: inserted_person.published, inbox_url: inserted_person.inbox_url.clone(), shared_inbox_url: None, @@ -303,17 +304,9 @@ mod tests { .await .unwrap(); - let person_form_1 = PersonInsertForm::builder() - .name("erich".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let person_form_1 = PersonInsertForm::test_form(inserted_instance.id, "erich"); let person_1 = Person::create(pool, &person_form_1).await.unwrap(); - let person_form_2 = PersonInsertForm::builder() - .name("michele".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let person_form_2 = PersonInsertForm::test_form(inserted_instance.id, "michele"); let person_2 = Person::create(pool, &person_form_2).await.unwrap(); let follow_form = PersonFollowerForm { diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index d5f1cba98..ac6cf76aa 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -401,11 +401,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("jim".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "jim"); let inserted_person = Person::create(pool, &new_person).await.unwrap(); diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index 260574bd2..7218ef468 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -101,11 +101,7 @@ mod tests { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) .await .unwrap(); - let person_form = PersonInsertForm::builder() - .name("jim".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let person_form = PersonInsertForm::test_form(inserted_instance.id, "jim"); let person = Person::create(pool, &person_form).await.unwrap(); let community_form = CommunityInsertForm::builder() diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index 75c7ce9bc..3cbfd052d 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -111,19 +111,11 @@ mod tests { .await .unwrap(); - let creator_form = PersonInsertForm::builder() - .name("creator_pm".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let creator_form = PersonInsertForm::test_form(inserted_instance.id, "creator_pm"); let inserted_creator = Person::create(pool, &creator_form).await.unwrap(); - let recipient_form = PersonInsertForm::builder() - .name("recipient_pm".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let recipient_form = PersonInsertForm::test_form(inserted_instance.id, "recipient_pm"); let inserted_recipient = Person::create(pool, &recipient_form).await.unwrap(); diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 2adba2ab2..206b3d842 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -309,6 +309,15 @@ diesel::table! { } } +diesel::table! { + image_details (link) { + link -> Text, + width -> Int4, + height -> Int4, + content_type -> Text, + } +} + diesel::table! { instance (id) { id -> Int4, @@ -856,8 +865,7 @@ diesel::table! { } diesel::table! { - remote_image (id) { - id -> Int4, + remote_image (link) { link -> Text, published -> Timestamptz, } @@ -1062,6 +1070,7 @@ diesel::allow_tables_to_appear_in_same_query!( federation_allowlist, federation_blocklist, federation_queue_state, + image_details, instance, instance_block, language, diff --git a/crates/db_schema/src/source/images.rs b/crates/db_schema/src/source/images.rs index 9d48e011b..0dea4b84f 100644 --- a/crates/db_schema/src/source/images.rs +++ b/crates/db_schema/src/source/images.rs @@ -1,13 +1,12 @@ use crate::newtypes::{DbUrl, LocalUserId}; #[cfg(feature = "full")] -use crate::schema::{local_image, remote_image}; +use crate::schema::{image_details, local_image, remote_image}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use std::fmt::Debug; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -30,7 +29,7 @@ pub struct LocalImage { pub published: DateTime, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = local_image))] pub struct LocalImageForm { @@ -46,15 +45,39 @@ pub struct LocalImageForm { #[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable))] #[cfg_attr(feature = "full", diesel(table_name = remote_image))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", diesel(primary_key(link)))] pub struct RemoteImage { - pub id: i32, pub link: DbUrl, pub published: DateTime, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = remote_image))] pub struct RemoteImageForm { pub link: DbUrl, } + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", ts(export))] +#[cfg_attr(feature = "full", diesel(table_name = image_details))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", diesel(primary_key(link)))] +pub struct ImageDetails { + pub link: DbUrl, + pub width: i32, + pub height: i32, + pub content_type: String, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = image_details))] +pub struct ImageDetailsForm { + pub link: DbUrl, + pub width: i32, + pub height: i32, + pub content_type: String, +} diff --git a/crates/db_schema/src/source/person.rs b/crates/db_schema/src/source/person.rs index 45eacd4fc..332b46eb5 100644 --- a/crates/db_schema/src/source/person.rs +++ b/crates/db_schema/src/source/person.rs @@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -60,33 +59,46 @@ pub struct Person { pub instance_id: InstanceId, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = person))] pub struct PersonInsertForm { - #[builder(!default)] pub name: String, - #[builder(!default)] pub public_key: String, - #[builder(!default)] pub instance_id: InstanceId, + #[new(default)] pub display_name: Option, + #[new(default)] pub avatar: Option, + #[new(default)] pub banned: Option, + #[new(default)] pub published: Option>, + #[new(default)] pub updated: Option>, + #[new(default)] pub actor_id: Option, + #[new(default)] pub bio: Option, + #[new(default)] pub local: Option, + #[new(default)] pub private_key: Option, + #[new(default)] pub last_refreshed_at: Option>, + #[new(default)] pub banner: Option, + #[new(default)] pub deleted: Option, + #[new(default)] pub inbox_url: Option, + #[new(default)] pub shared_inbox_url: Option, + #[new(default)] pub matrix_user_id: Option, + #[new(default)] pub bot_account: Option, + #[new(default)] pub ban_expires: Option>, } diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 928edd6a6..4b9ae2f07 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,7 +1,16 @@ -use crate::{newtypes::DbUrl, schema_setup, CommentSortType, SortType}; +use crate::{ + diesel::ExpressionMethods, + newtypes::{DbUrl, PersonId}, + schema::community, + schema_setup, + CommentSortType, + CommunityVisibility, + SortType, +}; use chrono::{DateTime, TimeDelta, Utc}; use deadpool::Runtime; use diesel::{ + dsl, helper_types::AsExprOf, pg::Pg, query_builder::{Query, QueryFragment}, @@ -29,6 +38,7 @@ use i_love_jesus::CursorKey; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, settings::SETTINGS, + utils::validation::clean_url_params, }; use once_cell::sync::Lazy; use regex::Regex; @@ -287,37 +297,35 @@ pub fn is_email_regex(test: &str) -> bool { EMAIL_REGEX.is_match(test) } -pub fn diesel_option_overwrite(opt: Option) -> Option> { +/// Takes an API text input, and converts it to an optional diesel DB update. +pub fn diesel_string_update(opt: Option<&str>) -> Option> { match opt { // An empty string is an erase - Some(unwrapped) => { - if !unwrapped.eq("") { - Some(Some(unwrapped)) - } else { - Some(None) - } - } + Some("") => Some(None), + Some(str) => Some(Some(str.into())), None => None, } } -pub fn diesel_option_overwrite_to_url(opt: &Option) -> LemmyResult>> { - match opt.as_ref().map(String::as_str) { +/// Takes an optional API URL-type input, and converts it to an optional diesel DB update. +/// Also cleans the url params. +pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult>> { + match opt { // An empty string is an erase Some("") => Ok(Some(None)), Some(str_url) => Url::parse(str_url) - .map(|u| Some(Some(u.into()))) + .map(|u| Some(Some(clean_url_params(&u).into()))) .with_lemmy_type(LemmyErrorType::InvalidUrl), None => Ok(None), } } -pub fn diesel_option_overwrite_to_url_create(opt: &Option) -> LemmyResult> { - match opt.as_ref().map(String::as_str) { - // An empty string is nothing - Some("") => Ok(None), +/// Takes an optional API URL-type input, and converts it to an optional diesel DB create. +/// Also cleans the url params. +pub fn diesel_url_create(opt: Option<&str>) -> LemmyResult> { + match opt { Some(str_url) => Url::parse(str_url) - .map(|u| Some(u.into())) + .map(|u| Some(clean_url_params(&u).into())) .with_lemmy_type(LemmyErrorType::InvalidUrl), None => Ok(None), } @@ -325,6 +333,10 @@ pub fn diesel_option_overwrite_to_url_create(opt: &Option) -> LemmyResul fn establish_connection(config: &str) -> BoxFuture> { let fut = async { + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rustls crypto provider"); + let rustls_config = DangerousClientConfigBuilder { cfg: ClientConfig::builder(), } @@ -568,8 +580,21 @@ impl Queries { } } +pub fn visible_communities_only(my_person_id: Option, query: Q) -> Q +where + Q: diesel::query_dsl::methods::FilterDsl< + dsl::Eq, + Output = Q, + >, +{ + if my_person_id.is_none() { + query.filter(community::visibility.eq(CommunityVisibility::Public)) + } else { + query + } +} + #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { @@ -593,26 +618,24 @@ mod tests { #[test] fn test_diesel_option_overwrite() { - assert_eq!(diesel_option_overwrite(None), None); - assert_eq!(diesel_option_overwrite(Some(String::new())), Some(None)); + assert_eq!(diesel_string_update(None), None); + assert_eq!(diesel_string_update(Some("")), Some(None)); assert_eq!( - diesel_option_overwrite(Some("test".to_string())), + diesel_string_update(Some("test")), Some(Some("test".to_string())) ); } #[test] - fn test_diesel_option_overwrite_to_url() { - assert!(matches!(diesel_option_overwrite_to_url(&None), Ok(None))); - assert!(matches!( - diesel_option_overwrite_to_url(&Some(String::new())), - Ok(Some(None)) - )); - assert!(diesel_option_overwrite_to_url(&Some("invalid_url".to_string())).is_err()); + fn test_diesel_option_overwrite_to_url() -> LemmyResult<()> { + assert!(matches!(diesel_url_update(None), Ok(None))); + assert!(matches!(diesel_url_update(Some("")), Ok(Some(None)))); + assert!(diesel_url_update(Some("invalid_url")).is_err()); let example_url = "https://example.com"; assert!(matches!( - diesel_option_overwrite_to_url(&Some(example_url.to_string())), - Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into() + diesel_url_update(Some(example_url)), + Ok(Some(Some(url))) if url == Url::parse(example_url)?.into() )); + Ok(()) } } diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index ebea1a5f4..950d061ba 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -297,11 +297,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("timmy_crv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_crv"); let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); @@ -319,20 +315,12 @@ mod tests { counts: Default::default(), }; - let new_person_2 = PersonInsertForm::builder() - .name("sara_crv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_crv"); let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); // Add a third person, since new ppl can only report something once. - let new_person_3 = PersonInsertForm::builder() - .name("jessica_crv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_crv"); let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap(); diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 7588943b9..cd7560a00 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -1,5 +1,4 @@ -use crate::structs::{CommentView, LocalUserView}; -use chrono::{DateTime, Utc}; +use crate::structs::CommentView; use diesel::{ dsl::{exists, not}, pg::Pg, @@ -17,6 +16,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions}; use lemmy_db_schema::{ + impls::local_user::LocalUserOptionHelper, newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId}, schema::{ comment, @@ -35,9 +35,18 @@ use lemmy_db_schema::{ person_block, post, }, - utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + source::local_user::LocalUser, + utils::{ + fuzzy_search, + limit_and_offset, + visible_communities_only, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, CommentSortType, - CommunityVisibility, ListingType, }; @@ -63,17 +72,6 @@ fn queries<'a>() -> Queries< ) }; - let is_saved = |person_id| { - comment_saved::table - .filter( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(person_id)), - ) - .select(comment_saved::published.nullable()) - .single_value() - }; - let is_community_followed = |person_id| { community_follower::table .filter( @@ -147,14 +145,6 @@ fn queries<'a>() -> Queries< Box::new(None::.into_sql::>()) }; - let is_saved_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(is_saved(person_id)) - } else { - Box::new(None::>.into_sql::>()) - }; - let is_creator_blocked_selection: Box> = if let Some(person_id) = my_person_id { Box::new(is_creator_blocked(person_id)) @@ -167,6 +157,13 @@ fn queries<'a>() -> Queries< .inner_join(post::table) .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(comment_aggregates::table) + .left_join( + comment_saved::table.on( + comment::id + .eq(comment_saved::comment_id) + .and(comment_saved::person_id.eq(my_person_id.unwrap_or(PersonId(-1)))), + ), + ) .select(( comment::all_columns, person::all_columns, @@ -178,7 +175,7 @@ fn queries<'a>() -> Queries< creator_is_moderator, creator_is_admin, subscribed_type_selection, - is_saved_selection.is_not_null(), + comment_saved::person_id.nullable().is_not_null(), is_creator_blocked_selection, score_selection, )) @@ -187,22 +184,19 @@ fn queries<'a>() -> Queries< let read = move |mut conn: DbConn<'a>, (comment_id, my_person_id): (CommentId, Option)| async move { let mut query = all_joins(comment::table.find(comment_id).into_boxed(), my_person_id); - // Hide local only communities from unauthenticated users - if my_person_id.is_none() { - query = query.filter(community::visibility.eq(CommunityVisibility::Public)); - } + query = visible_communities_only(my_person_id, query); query.first(&mut conn).await }; let list = move |mut conn: DbConn<'a>, options: CommentQuery<'a>| async move { - let my_person_id = options.local_user.map(|l| l.person.id); - let my_local_user_id = options.local_user.map(|l| l.local_user.id); - // The left join below will return None in this case - let person_id_join = my_person_id.unwrap_or(PersonId(-1)); - let local_user_id_join = my_local_user_id.unwrap_or(LocalUserId(-1)); + let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); + let local_user_id_join = options + .local_user + .local_user_id() + .unwrap_or(LocalUserId(-1)); - let mut query = all_joins(comment::table.into_boxed(), my_person_id); + let mut query = all_joins(comment::table.into_boxed(), options.local_user.person_id()); if let Some(creator_id) = options.creator_id { query = query.filter(comment::creator_id.eq(creator_id)); @@ -258,26 +252,22 @@ fn queries<'a>() -> Queries< } // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only { + if options.saved_only.unwrap_or_default() { query = query - .filter(is_saved(person_id_join).is_not_null()) - .then_order_by(is_saved(person_id_join).desc()); + .filter(comment_saved::person_id.is_not_null()) + .then_order_by(comment_saved::published.desc()); } - if let Some(my_id) = my_person_id { + if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); - if options.liked_only { + if options.liked_only.unwrap_or_default() { query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); - } else if options.disliked_only { + } else if options.disliked_only.unwrap_or_default() { query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); } } - if !options - .local_user - .map(|l| l.local_user.show_bot_accounts) - .unwrap_or(true) - { + if !options.local_user.show_bot_accounts() { query = query.filter(person::bot_account.eq(false)); }; @@ -311,10 +301,7 @@ fn queries<'a>() -> Queries< query = query.filter(not(is_creator_blocked(person_id_join))); }; - // Hide comments in local only communities from unauthenticated users - if options.local_user.is_none() { - query = query.filter(community::visibility.eq(CommunityVisibility::Public)); - } + query = visible_communities_only(options.local_user.person_id(), query); // A Max depth given means its a tree fetch let (limit, offset) = if let Some(max_depth) = options.max_depth { @@ -409,11 +396,11 @@ pub struct CommentQuery<'a> { pub post_id: Option, pub parent_path: Option, pub creator_id: Option, - pub local_user: Option<&'a LocalUserView>, + pub local_user: Option<&'a LocalUser>, pub search_term: Option, - pub saved_only: bool, - pub liked_only: bool, - pub disliked_only: bool, + pub saved_only: Option, + pub liked_only: Option, + pub disliked_only: Option, pub page: Option, pub limit: Option, pub max_depth: Option, @@ -501,11 +488,7 @@ mod tests { async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let timmy_person_form = PersonInsertForm::builder() - .name("timmy".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy"); let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?; let timmy_local_user_form = LocalUserInsertForm::builder() .person_id(inserted_timmy_person.id) @@ -514,11 +497,7 @@ mod tests { .build(); let inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; - let sara_person_form = PersonInsertForm::builder() - .name("sara".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara"); let inserted_sara_person = Person::create(pool, &sara_person_form).await?; let new_community = CommunityInsertForm::builder() @@ -680,7 +659,7 @@ mod tests { let read_comment_views_with_person = CommentQuery { sort: (Some(CommentSortType::Old)), post_id: (Some(data.inserted_post.id)), - local_user: (Some(&data.timmy_local_user_view)), + local_user: (Some(&data.timmy_local_user_view.local_user)), ..Default::default() } .list(pool) @@ -732,8 +711,8 @@ mod tests { CommentLike::like(pool, &comment_like_form).await?; let read_liked_comment_views = CommentQuery { - local_user: (Some(&data.timmy_local_user_view)), - liked_only: (true), + local_user: Some(&data.timmy_local_user_view.local_user), + liked_only: Some(true), ..Default::default() } .list(pool) @@ -748,8 +727,8 @@ mod tests { assert_length!(1, read_liked_comment_views); let read_disliked_comment_views: Vec = CommentQuery { - local_user: (Some(&data.timmy_local_user_view)), - disliked_only: (true), + local_user: Some(&data.timmy_local_user_view.local_user), + disliked_only: Some(true), ..Default::default() } .list(pool) @@ -843,7 +822,7 @@ mod tests { // by default, user has all languages enabled and should see all comments // (except from blocked user) let all_languages = CommentQuery { - local_user: (Some(&data.timmy_local_user_view)), + local_user: (Some(&data.timmy_local_user_view.local_user)), ..Default::default() } .list(pool) @@ -861,7 +840,7 @@ mod tests { ) .await?; let finnish_comments = CommentQuery { - local_user: (Some(&data.timmy_local_user_view)), + local_user: (Some(&data.timmy_local_user_view.local_user)), ..Default::default() } .list(pool) @@ -887,7 +866,7 @@ mod tests { ) .await?; let undetermined_comment = CommentQuery { - local_user: (Some(&data.timmy_local_user_view)), + local_user: (Some(&data.timmy_local_user_view.local_user)), ..Default::default() } .list(pool) @@ -1000,8 +979,8 @@ mod tests { // Fetch the saved comments let comments = CommentQuery { - local_user: Some(&data.timmy_local_user_view), - saved_only: true, + local_user: Some(&data.timmy_local_user_view.local_user), + saved_only: Some(true), ..Default::default() } .list(pool) @@ -1179,7 +1158,7 @@ mod tests { assert_eq!(0, unauthenticated_query.len()); let authenticated_query = CommentQuery { - local_user: Some(&data.timmy_local_user_view), + local_user: Some(&data.timmy_local_user_view.local_user), ..Default::default() } .list(pool) diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index ac60deff9..e89b7d545 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -319,11 +319,7 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("timmy_prv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_prv"); let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); @@ -341,20 +337,12 @@ mod tests { counts: Default::default(), }; - let new_person_2 = PersonInsertForm::builder() - .name("sara_prv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_prv"); let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); // Add a third person, since new ppl can only report something once. - let new_person_3 = PersonInsertForm::builder() - .name("jessica_prv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_prv"); let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap(); diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index afb0f435f..0e22f689b 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -1,5 +1,4 @@ -use crate::structs::{LocalUserView, PaginationCursor, PostView}; -use chrono::{DateTime, Utc}; +use crate::structs::{PaginationCursor, PostView}; use diesel::{ debug_query, dsl::{exists, not, IntervalDsl}, @@ -21,6 +20,7 @@ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ aggregates::structs::{post_aggregates_keys as key, PostAggregates}, + impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, LocalUserId, PersonId, PostId}, schema::{ community, @@ -28,6 +28,7 @@ use lemmy_db_schema::{ community_follower, community_moderator, community_person_ban, + image_details, instance_block, local_user, local_user_language, @@ -41,13 +42,14 @@ use lemmy_db_schema::{ post_read, post_saved, }, - source::site::Site, + source::{local_user::LocalUser, site::Site}, utils::{ functions::coalesce, fuzzy_search, get_conn, limit_and_offset, now, + visible_communities_only, Commented, DbConn, DbPool, @@ -56,7 +58,6 @@ use lemmy_db_schema::{ ReadFn, ReverseTimestampKey, }, - CommunityVisibility, ListingType, SortType, }; @@ -100,17 +101,6 @@ fn queries<'a>() -> Queries< ), ); - let is_saved = |person_id| { - post_saved::table - .filter( - post_aggregates::post_id - .eq(post_saved::post_id) - .and(post_saved::person_id.eq(person_id)), - ) - .select(post_saved::published.nullable()) - .single_value() - }; - let is_read = |person_id| { exists( post_read::table.filter( @@ -162,14 +152,6 @@ fn queries<'a>() -> Queries< Box::new(false.into_sql::()) }; - let is_saved_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(is_saved(person_id)) - } else { - Box::new(None::>.into_sql::>()) - }; - let is_read_selection: Box> = if let Some(person_id) = my_person_id { Box::new(is_read(person_id)) @@ -237,17 +219,26 @@ fn queries<'a>() -> Queries< .inner_join(person::table) .inner_join(community::table) .inner_join(post::table) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + .left_join( + post_saved::table.on( + post_aggregates::post_id + .eq(post_saved::post_id) + .and(post_saved::person_id.eq(my_person_id.unwrap_or(PersonId(-1)))), + ), + ) .select(( post::all_columns, person::all_columns, community::all_columns, + image_details::all_columns.nullable(), is_creator_banned_from_community, is_local_user_banned_from_community_selection, creator_is_moderator, creator_is_admin, post_aggregates::all_columns, subscribed_type_selection, - is_saved_selection.is_not_null(), + post_saved::person_id.nullable().is_not_null(), is_read_selection, is_hidden_selection, is_creator_blocked_selection, @@ -298,10 +289,7 @@ fn queries<'a>() -> Queries< ); } - // Hide posts in local only communities from unauthenticated users - if my_person_id.is_none() { - query = query.filter(community::visibility.eq(CommunityVisibility::Public)); - } + query = visible_communities_only(my_person_id, query); Commented::new(query) .text("PostView::read") @@ -310,31 +298,30 @@ fn queries<'a>() -> Queries< }; let list = move |mut conn: DbConn<'a>, (options, site): (PostQuery<'a>, &'a Site)| async move { - let my_person_id = options.local_user.map(|l| l.person.id); - let my_local_user_id = options.local_user.map(|l| l.local_user.id); - // The left join below will return None in this case - let person_id_join = my_person_id.unwrap_or(PersonId(-1)); - let local_user_id_join = my_local_user_id.unwrap_or(LocalUserId(-1)); + let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); + let local_user_id_join = options + .local_user + .local_user_id() + .unwrap_or(LocalUserId(-1)); - let mut query = all_joins(post_aggregates::table.into_boxed(), my_person_id); + let mut query = all_joins( + post_aggregates::table.into_boxed(), + options.local_user.person_id(), + ); // hide posts from deleted communities query = query.filter(community::deleted.eq(false)); // only show deleted posts to creator - if let Some(person_id) = my_person_id { + if let Some(person_id) = options.local_user.person_id() { query = query.filter(post::deleted.eq(false).or(post::creator_id.eq(person_id))); } else { query = query.filter(post::deleted.eq(false)); } - let is_admin = options - .local_user - .map(|l| l.local_user.admin) - .unwrap_or(false); // only show removed posts to admin when viewing user profile - if !(options.creator_id.is_some() && is_admin) { + if !(options.creator_id.is_some() && options.local_user.is_admin()) { query = query .filter(community::removed.eq(false)) .filter(post::removed.eq(false)); @@ -348,7 +335,7 @@ fn queries<'a>() -> Queries< } if let Some(listing_type) = options.listing_type { - if let Some(person_id) = my_person_id { + if let Some(person_id) = options.local_user.person_id() { let is_subscribed = exists( community_follower::table.filter( post_aggregates::community_id @@ -405,70 +392,56 @@ fn queries<'a>() -> Queries< .filter(not(post::removed.or(post::deleted))); } - // If there is a content warning, show nsfw content by default. - let has_content_warning = site.content_warning.is_some(); - if !options - .local_user - .map(|l| l.local_user.show_nsfw) - .unwrap_or(has_content_warning) - { + if !options.local_user.show_nsfw(site) { query = query .filter(post::nsfw.eq(false)) .filter(community::nsfw.eq(false)); }; - if !options - .local_user - .map(|l| l.local_user.show_bot_accounts) - .unwrap_or(true) - { + if !options.local_user.show_bot_accounts() { query = query.filter(person::bot_account.eq(false)); }; // If its saved only, then filter, and order by the saved time, not the comment creation time. - if let (true, Some(person_id)) = (options.saved_only, my_person_id) { + if options.saved_only.unwrap_or_default() { query = query - .filter(is_saved(person_id).is_not_null()) - .then_order_by(is_saved(person_id).desc()); + .filter(post_saved::person_id.is_not_null()) + .then_order_by(post_saved::published.desc()); } // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read // setting wont be able to see saved posts. else if !options - .local_user - .map(|l| l.local_user.show_read_posts) - .unwrap_or(true) + .show_read + .unwrap_or(options.local_user.show_read_posts()) { // Do not hide read posts when it is a user profile view // Or, only hide read posts on non-profile views - if let (None, Some(person_id)) = (options.creator_id, my_person_id) { + if let (None, Some(person_id)) = (options.creator_id, options.local_user.person_id()) { query = query.filter(not(is_read(person_id))); } } - if !options.show_hidden { + if !options.show_hidden.unwrap_or_default() { // If a creator id isn't given (IE its on home or community pages), hide the hidden posts - if let (None, Some(person_id)) = (options.creator_id, my_person_id) { + if let (None, Some(person_id)) = (options.creator_id, options.local_user.person_id()) { query = query.filter(not(is_hidden(person_id))); } } - if let Some(my_id) = my_person_id { + if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = post_aggregates::creator_id.ne(my_id); - if options.liked_only { + if options.liked_only.unwrap_or_default() { query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); - } else if options.disliked_only { + } else if options.disliked_only.unwrap_or_default() { query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); } }; - // Hide posts in local only communities from unauthenticated users - if options.local_user.is_none() { - query = query.filter(community::visibility.eq(CommunityVisibility::Public)); - } + query = visible_communities_only(options.local_user.person_id(), query); // Dont filter blocks or missing languages for moderator view type if let (Some(person_id), false) = ( - my_person_id, + options.local_user.person_id(), options.listing_type.unwrap_or_default() == ListingType::ModeratorView, ) { // Filter out the rows with missing languages @@ -506,7 +479,7 @@ fn queries<'a>() -> Queries< let page_after = options.page_after.map(|c| c.0); let page_before_or_equal = options.page_before_or_equal.map(|c| c.0); - if options.page_back { + if options.page_back.unwrap_or_default() { query = query .before(page_after) .after_or_equal(page_before_or_equal) @@ -631,18 +604,19 @@ pub struct PostQuery<'a> { // if true, the query should be handled as if community_id was not given except adding the // literal filter pub community_id_just_for_prefetch: bool, - pub local_user: Option<&'a LocalUserView>, + pub local_user: Option<&'a LocalUser>, pub search_term: Option, pub url_search: Option, - pub saved_only: bool, - pub liked_only: bool, - pub disliked_only: bool, + pub saved_only: Option, + pub liked_only: Option, + pub disliked_only: Option, pub page: Option, pub limit: Option, pub page_after: Option, pub page_before_or_equal: Option, - pub page_back: bool, - pub show_hidden: bool, + pub page_back: Option, + pub show_hidden: Option, + pub show_read: Option, } impl<'a> PostQuery<'a> { @@ -676,11 +650,7 @@ impl<'a> PostQuery<'a> { "legacy pagination cannot be combined with v2 pagination".into(), )); } - let self_person_id = self - .local_user - .expect("part of the above if") - .local_user - .person_id; + let self_person_id = self.local_user.expect("part of the above if").person_id; let largest_subscribed = { let conn = &mut get_conn(pool).await?; community_follower @@ -717,7 +687,7 @@ impl<'a> PostQuery<'a> { if (v.len() as i64) < limit { Ok(Some(self.clone())) } else { - let item = if self.page_back { + let item = if self.page_back.unwrap_or_default() { // for backward pagination, get first element instead v.into_iter().next() } else { @@ -820,7 +790,7 @@ mod tests { fn default_post_query(&self) -> PostQuery<'_> { PostQuery { sort: Some(SortType::New), - local_user: Some(&self.local_user_view), + local_user: Some(&self.local_user_view.local_user), ..Default::default() } } @@ -1156,7 +1126,7 @@ mod tests { // Read the liked only let read_liked_post_listing = PostQuery { community_id: Some(data.inserted_community.id), - liked_only: true, + liked_only: Some(true), ..data.default_post_query() } .list(&data.site, pool) @@ -1167,7 +1137,7 @@ mod tests { let read_disliked_post_listing = PostQuery { community_id: Some(data.inserted_community.id), - disliked_only: true, + disliked_only: Some(true), ..data.default_post_query() } .list(&data.site, pool) @@ -1337,8 +1307,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), false), - (Some(&data.local_user_view), true), + (Some(&data.blocked_local_user_view.local_user), false), + (Some(&data.local_user_view.local_user), true), ] { let contains_deleted = PostQuery { local_user, @@ -1493,7 +1463,7 @@ mod tests { loop { let post_listings = PostQuery { page_after: page_before, - page_back: true, + page_back: Some(true), ..options.clone() } .list(&data.site, pool) @@ -1551,6 +1521,26 @@ mod tests { let post_listings_hide_read = data.default_post_query().list(&data.site, pool).await?; assert_eq!(vec![POST], names(&post_listings_hide_read)); + // Test with the show_read override as true + let post_listings_show_read_true = PostQuery { + show_read: Some(true), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + assert_eq!( + vec![POST_BY_BOT, POST], + names(&post_listings_show_read_true) + ); + + // Test with the show_read override as false + let post_listings_show_read_false = PostQuery { + show_read: Some(false), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + assert_eq!(vec![POST], names(&post_listings_show_read_false)); cleanup(data, pool).await } @@ -1576,8 +1566,8 @@ mod tests { // Make sure it does come back with the show_hidden option let post_listings_show_hidden = PostQuery { sort: Some(SortType::New), - local_user: Some(&data.local_user_view), - show_hidden: true, + local_user: Some(&data.local_user_view.local_user), + show_hidden: Some(true), ..Default::default() } .list(&data.site, pool) @@ -1668,6 +1658,7 @@ mod tests { public_key: inserted_person.public_key.clone(), last_refreshed_at: inserted_person.last_refreshed_at, }, + image_details: None, creator_banned_from_community: false, banned_from_community: false, creator_is_moderator: false, @@ -1751,7 +1742,7 @@ mod tests { assert_eq!(0, unauthenticated_query.len()); let authenticated_query = PostQuery { - local_user: Some(&data.local_user_view), + local_user: Some(&data.local_user_view.local_user), ..Default::default() } .list(&data.site, pool) diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index 6011574e6..f5e70fb3e 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -140,18 +140,10 @@ mod tests { .await .unwrap(); - let new_person_1 = PersonInsertForm::builder() - .name("timmy_mrv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person_1 = PersonInsertForm::test_form(inserted_instance.id, "timmy_mrv"); let inserted_timmy = Person::create(pool, &new_person_1).await.unwrap(); - let new_person_2 = PersonInsertForm::builder() - .name("jessica_mrv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); let inserted_jessica = Person::create(pool, &new_person_2).await.unwrap(); // timmy sends private message to jessica @@ -184,11 +176,7 @@ mod tests { assert_eq!(pm_report.reason, reports[0].private_message_report.reason); assert_eq!(pm.content, reports[0].private_message.content); - let new_person_3 = PersonInsertForm::builder() - .name("admin_mrv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "admin_mrv"); let inserted_admin = Person::create(pool, &new_person_3).await.unwrap(); // admin resolves the report (after taking appropriate action) diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 764ef1dcb..79224d86f 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -209,27 +209,15 @@ mod tests { .await .unwrap(); - let timmy_form = PersonInsertForm::builder() - .name("timmy_rav".into()) - .public_key("pubkey".to_string()) - .instance_id(instance.id) - .build(); + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_rav"); let timmy = Person::create(pool, &timmy_form).await.unwrap(); - let sara_form = PersonInsertForm::builder() - .name("sara_rav".into()) - .public_key("pubkey".to_string()) - .instance_id(instance.id) - .build(); + let sara_form = PersonInsertForm::test_form(instance.id, "sara_rav"); let sara = Person::create(pool, &sara_form).await.unwrap(); - let jess_form = PersonInsertForm::builder() - .name("jess_rav".into()) - .public_key("pubkey".to_string()) - .instance_id(instance.id) - .build(); + let jess_form = PersonInsertForm::test_form(instance.id, "jess_rav"); let jess = Person::create(pool, &jess_form).await.unwrap(); diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index 65629d65c..cd63859af 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -163,11 +163,7 @@ mod tests { .await .unwrap(); - let timmy_person_form = PersonInsertForm::builder() - .name("timmy_rav".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rav"); let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap(); @@ -181,11 +177,7 @@ mod tests { .await .unwrap(); - let sara_person_form = PersonInsertForm::builder() - .name("sara_rav".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rav"); let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap(); @@ -213,11 +205,7 @@ mod tests { .unwrap() .unwrap(); - let jess_person_form = PersonInsertForm::builder() - .name("jess_rav".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let jess_person_form = PersonInsertForm::test_form(inserted_instance.id, "jess_rav"); let inserted_jess_person = Person::create(pool, &jess_person_form).await.unwrap(); diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 350e4cad4..3c219d63f 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -8,7 +8,7 @@ use lemmy_db_schema::{ community::Community, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, - images::LocalImage, + images::{ImageDetails, LocalImage}, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, @@ -131,6 +131,7 @@ pub struct PostView { pub post: Post, pub creator: Person, pub community: Community, + pub image_details: Option, pub creator_banned_from_community: bool, pub banned_from_community: bool, pub creator_is_moderator: bool, diff --git a/crates/db_views/src/vote_view.rs b/crates/db_views/src/vote_view.rs index a0441ff4e..5daa072c3 100644 --- a/crates/db_views/src/vote_view.rs +++ b/crates/db_views/src/vote_view.rs @@ -112,19 +112,11 @@ mod tests { .await .unwrap(); - let new_person = PersonInsertForm::builder() - .name("timmy_vv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_vv"); let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); - let new_person_2 = PersonInsertForm::builder() - .name("sara_vv".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_vv"); let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index 1892055d1..d9e6a3352 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -40,6 +40,7 @@ serial_test = { workspace = true } tokio = { workspace = true } pretty_assertions = { workspace = true } url.workspace = true +lemmy_db_views.workspace = true lemmy_utils.workspace = true [package.metadata.cargo-machete] diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 547c00e53..b1d95e719 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -31,6 +31,7 @@ use lemmy_db_schema::{ person_block, post, }, + source::local_user::LocalUser, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, }; @@ -193,6 +194,8 @@ fn queries<'a>() -> Queries< }; let list = move |mut conn: DbConn<'a>, options: CommentReplyQuery| async move { + // These filters need to be kept in sync with the filters in + // CommentReplyView::get_unread_replies() let mut query = all_joins(comment_reply::table.into_boxed(), options.my_person_id); if let Some(recipient_id) = options.recipient_id { @@ -204,7 +207,7 @@ fn queries<'a>() -> Queries< } if !options.show_bot_accounts { - query = query.filter(person::bot_account.eq(false)); + query = query.filter(not(person::bot_account)); }; query = match options.sort.unwrap_or(CommentSortType::New) { @@ -246,24 +249,33 @@ impl CommentReplyView { /// Gets the number of unread replies pub async fn get_unread_replies( pool: &mut DbPool<'_>, - my_person_id: PersonId, + local_user: &LocalUser, ) -> Result { use diesel::dsl::count; let conn = &mut get_conn(pool).await?; - comment_reply::table + let mut query = comment_reply::table .inner_join(comment::table) .left_join( person_block::table.on( comment::creator_id .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), + .and(person_block::person_id.eq(local_user.person_id)), ), ) - // Dont count replies from blocked users + .inner_join(person::table.on(comment::creator_id.eq(person::id))) + .into_boxed(); + + // These filters need to be kept in sync with the filters in queries().list() + if !local_user.show_bot_accounts { + query = query.filter(not(person::bot_account)); + } + + query + // Don't count replies from blocked users .filter(person_block::person_id.is_null()) - .filter(comment_reply::recipient_id.eq(my_person_id)) + .filter(comment_reply::recipient_id.eq(local_user.person_id)) .filter(comment_reply::read.eq(false)) .filter(comment::deleted.eq(false)) .filter(comment::removed.eq(false)) @@ -301,13 +313,15 @@ mod tests { comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm}, community::{Community, CommunityInsertForm}, instance::Instance, - person::{Person, PersonInsertForm}, + local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, + person::{Person, PersonInsertForm, PersonUpdateForm}, person_block::{PersonBlock, PersonBlockForm}, post::{Post, PostInsertForm}, }, traits::{Blockable, Crud}, utils::build_db_pool_for_tests, }; + use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use pretty_assertions::assert_eq; use serial_test::serial; @@ -320,22 +334,20 @@ mod tests { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let terry_form = PersonInsertForm::builder() - .name("terrylake".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let terry_form = PersonInsertForm::test_form(inserted_instance.id, "terrylake"); let inserted_terry = Person::create(pool, &terry_form).await?; - let recipient_form = PersonInsertForm::builder() - .name("terrylakes recipient".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let recipient_form = PersonInsertForm { + local: Some(true), + ..PersonInsertForm::test_form(inserted_instance.id, "terrylakes recipient") + }; let inserted_recipient = Person::create(pool, &recipient_form).await?; let recipient_id = inserted_recipient.id; + let recipient_local_user = + LocalUser::create(pool, &LocalUserInsertForm::test_form(recipient_id), vec![]).await?; + let new_community = CommunityInsertForm::builder() .name("test community lake".to_string()) .title("nada".to_owned()) @@ -386,7 +398,7 @@ mod tests { CommentReply::update(pool, inserted_reply.id, &comment_reply_update_form).await?; // Test to make sure counts and blocks work correctly - let unread_replies = CommentReplyView::get_unread_replies(pool, recipient_id).await?; + let unread_replies = CommentReplyView::get_unread_replies(pool, &recipient_local_user).await?; let query = CommentReplyQuery { recipient_id: Some(recipient_id), @@ -409,11 +421,44 @@ mod tests { PersonBlock::block(pool, &block_form).await?; let unread_replies_after_block = - CommentReplyView::get_unread_replies(pool, recipient_id).await?; - let replies_after_block = query.list(pool).await?; + CommentReplyView::get_unread_replies(pool, &recipient_local_user).await?; + let replies_after_block = query.clone().list(pool).await?; assert_eq!(0, unread_replies_after_block); assert_eq!(0, replies_after_block.len()); + // Unblock user so we can reuse the same person + PersonBlock::unblock(pool, &block_form).await?; + + // Turn Terry into a bot account + let person_update_form = PersonUpdateForm { + bot_account: Some(true), + ..Default::default() + }; + Person::update(pool, inserted_terry.id, &person_update_form).await?; + + let recipient_local_user_update_form = LocalUserUpdateForm { + show_bot_accounts: Some(false), + ..Default::default() + }; + LocalUser::update( + pool, + recipient_local_user.id, + &recipient_local_user_update_form, + ) + .await?; + let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id) + .await? + .ok_or(LemmyErrorType::CouldntFindLocalUser)?; + + let unread_replies_after_hide_bots = + CommentReplyView::get_unread_replies(pool, &recipient_local_user_view.local_user).await?; + + let mut query_without_bots = query.clone(); + query_without_bots.show_bot_accounts = false; + let replies_after_hide_bots = query_without_bots.list(pool).await?; + assert_eq!(0, unread_replies_after_hide_bots); + assert_eq!(0, replies_after_hide_bots.len()); + Comment::delete(pool, inserted_comment.id).await?; Post::delete(pool, inserted_post.id).await?; Community::delete(pool, inserted_community.id).await?; diff --git a/crates/db_views_actor/src/community_moderator_view.rs b/crates/db_views_actor/src/community_moderator_view.rs index 808fc0340..f58e3fee0 100644 --- a/crates/db_views_actor/src/community_moderator_view.rs +++ b/crates/db_views_actor/src/community_moderator_view.rs @@ -90,7 +90,7 @@ impl CommunityModeratorView { .distinct_on(community_moderator::community_id) .order_by(( community_moderator::community_id, - community_moderator::person_id, + community_moderator::published, )) .load::(conn) .await diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 0ff421540..25e76c7b3 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -11,6 +11,7 @@ use diesel::{ }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ + impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, schema::{ community, @@ -19,11 +20,18 @@ use lemmy_db_schema::{ community_follower, community_person_ban, instance_block, - local_user, }, source::{community::CommunityFollower, local_user::LocalUser, site::Site}, - utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, - CommunityVisibility, + utils::{ + fuzzy_search, + limit_and_offset, + visible_communities_only, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, ListingType, SortType, }; @@ -97,10 +105,7 @@ fn queries<'a>() -> Queries< query = query.filter(not_removed_or_deleted); } - // Hide local only communities from unauthenticated users - if my_person_id.is_none() { - query = query.filter(community::visibility.eq(CommunityVisibility::Public)); - } + query = visible_communities_only(my_person_id, query); query.first(&mut conn).await }; @@ -108,14 +113,14 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { use SortType::*; - let my_person_id = options.local_user.map(|l| l.person_id); - // The left join below will return None in this case - let person_id_join = my_person_id.unwrap_or(PersonId(-1)); + let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); - let mut query = all_joins(community::table.into_boxed(), my_person_id) - .left_join(local_user::table.on(local_user::person_id.eq(person_id_join))) - .select(selection); + let mut query = all_joins( + community::table.into_boxed(), + options.local_user.person_id(), + ) + .select(selection); if let Some(search_term) = options.search_term { let searcher = fuzzy_search(&search_term); @@ -162,21 +167,14 @@ fn queries<'a>() -> Queries< // Don't show blocked communities and communities on blocked instances. nsfw communities are // also hidden (based on profile setting) - if options.local_user.is_some() { - query = query.filter(instance_block::person_id.is_null()); - query = query.filter(community_block::person_id.is_null()); - query = query.filter(community::nsfw.eq(false).or(local_user::show_nsfw.eq(true))); - } else { - // No person in request, only show nsfw communities if show_nsfw is passed into request or if - // site has content warning. - let has_content_warning = site.content_warning.is_some(); - if !options.show_nsfw && !has_content_warning { - query = query.filter(community::nsfw.eq(false)); - } - // Hide local only communities from unauthenticated users - query = query.filter(community::visibility.eq(CommunityVisibility::Public)); + query = query.filter(instance_block::person_id.is_null()); + query = query.filter(community_block::person_id.is_null()); + if !(options.local_user.show_nsfw(site) || options.show_nsfw) { + query = query.filter(community::nsfw.eq(false)); } + query = visible_communities_only(options.local_user.person_id(), query); + let (limit, offset) = limit_and_offset(options.page, options.limit)?; query .limit(limit) @@ -286,11 +284,7 @@ mod tests { let person_name = "tegan".to_string(); - let new_person = PersonInsertForm::builder() - .name(person_name.clone()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, &person_name); let inserted_person = Person::create(pool, &new_person).await.unwrap(); diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index d42987a68..d6fd7363d 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -31,6 +31,7 @@ use lemmy_db_schema::{ person_mention, post, }, + source::local_user::LocalUser, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, }; @@ -192,6 +193,8 @@ fn queries<'a>() -> Queries< }; let list = move |mut conn: DbConn<'a>, options: PersonMentionQuery| async move { + // These filters need to be kept in sync with the filters in + // PersonMentionView::get_unread_mentions() let mut query = all_joins(person_mention::table.into_boxed(), options.my_person_id); if let Some(recipient_id) = options.recipient_id { @@ -203,7 +206,7 @@ fn queries<'a>() -> Queries< } if !options.show_bot_accounts { - query = query.filter(person::bot_account.eq(false)); + query = query.filter(not(person::bot_account)); }; query = match options.sort.unwrap_or(CommentSortType::Hot) { @@ -247,23 +250,32 @@ impl PersonMentionView { /// Gets the number of unread mentions pub async fn get_unread_mentions( pool: &mut DbPool<'_>, - my_person_id: PersonId, + local_user: &LocalUser, ) -> Result { use diesel::dsl::count; let conn = &mut get_conn(pool).await?; - person_mention::table + let mut query = person_mention::table .inner_join(comment::table) .left_join( person_block::table.on( comment::creator_id .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), + .and(person_block::person_id.eq(local_user.person_id)), ), ) - // Dont count replies from blocked users + .inner_join(person::table.on(comment::creator_id.eq(person::id))) + .into_boxed(); + + // These filters need to be kept in sync with the filters in queries().list() + if !local_user.show_bot_accounts { + query = query.filter(not(person::bot_account)); + } + + query + // Don't count replies from blocked users .filter(person_block::person_id.is_null()) - .filter(person_mention::recipient_id.eq(my_person_id)) + .filter(person_mention::recipient_id.eq(local_user.person_id)) .filter(person_mention::read.eq(false)) .filter(comment::deleted.eq(false)) .filter(comment::removed.eq(false)) @@ -300,7 +312,8 @@ mod tests { comment::{Comment, CommentInsertForm}, community::{Community, CommunityInsertForm}, instance::Instance, - person::{Person, PersonInsertForm}, + local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, + person::{Person, PersonInsertForm, PersonUpdateForm}, person_block::{PersonBlock, PersonBlockForm}, person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm}, post::{Post, PostInsertForm}, @@ -308,6 +321,7 @@ mod tests { traits::{Blockable, Crud}, utils::build_db_pool_for_tests, }; + use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use pretty_assertions::assert_eq; use serial_test::serial; @@ -320,23 +334,18 @@ mod tests { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let new_person = PersonInsertForm::builder() - .name("terrylake".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "terrylake"); let inserted_person = Person::create(pool, &new_person).await?; - let recipient_form = PersonInsertForm::builder() - .name("terrylakes recipient".into()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .build(); + let recipient_form = PersonInsertForm::test_form(inserted_instance.id, "terrylakes recipient"); let inserted_recipient = Person::create(pool, &recipient_form).await?; let recipient_id = inserted_recipient.id; + let recipient_local_user = + LocalUser::create(pool, &LocalUserInsertForm::test_form(recipient_id), vec![]).await?; + let new_community = CommunityInsertForm::builder() .name("test community lake".to_string()) .title("nada".to_owned()) @@ -387,7 +396,8 @@ mod tests { PersonMention::update(pool, inserted_mention.id, &person_mention_update_form).await?; // Test to make sure counts and blocks work correctly - let unread_mentions = PersonMentionView::get_unread_mentions(pool, recipient_id).await?; + let unread_mentions = + PersonMentionView::get_unread_mentions(pool, &recipient_local_user).await?; let query = PersonMentionQuery { recipient_id: Some(recipient_id), @@ -410,11 +420,44 @@ mod tests { PersonBlock::block(pool, &block_form).await?; let unread_mentions_after_block = - PersonMentionView::get_unread_mentions(pool, recipient_id).await?; - let mentions_after_block = query.list(pool).await?; + PersonMentionView::get_unread_mentions(pool, &recipient_local_user).await?; + let mentions_after_block = query.clone().list(pool).await?; assert_eq!(0, unread_mentions_after_block); assert_eq!(0, mentions_after_block.len()); + // Unblock user so we can reuse the same person + PersonBlock::unblock(pool, &block_form).await?; + + // Turn Terry into a bot account + let person_update_form = PersonUpdateForm { + bot_account: Some(true), + ..Default::default() + }; + Person::update(pool, inserted_person.id, &person_update_form).await?; + + let recipient_local_user_update_form = LocalUserUpdateForm { + show_bot_accounts: Some(false), + ..Default::default() + }; + LocalUser::update( + pool, + recipient_local_user.id, + &recipient_local_user_update_form, + ) + .await?; + let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id) + .await? + .ok_or(LemmyErrorType::CouldntFindLocalUser)?; + + let unread_mentions_after_hide_bots = + PersonMentionView::get_unread_mentions(pool, &recipient_local_user_view.local_user).await?; + + let mut query_without_bots = query.clone(); + query_without_bots.show_bot_accounts = false; + let replies_after_hide_bots = query_without_bots.list(pool).await?; + assert_eq!(0, unread_mentions_after_hide_bots); + assert_eq!(0, replies_after_hide_bots.len()); + Comment::delete(pool, inserted_comment.id).await?; Post::delete(pool, inserted_post.id).await?; Community::delete(pool, inserted_community.id).await?; diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 5734bc812..98a0ca38d 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -191,12 +191,10 @@ mod tests { 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::builder() - .name("alice".to_string()) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .local(Some(true)) - .build(); + 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::builder() .person_id(alice.id) @@ -204,13 +202,11 @@ mod tests { .build(); let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?; - let bob_form = PersonInsertForm::builder() - .name("bob".to_string()) - .bot_account(Some(true)) - .public_key("pubkey".to_string()) - .instance_id(inserted_instance.id) - .local(Some(false)) - .build(); + 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::builder() .person_id(bob.id) diff --git a/crates/federate/src/lib.rs b/crates/federate/src/lib.rs index d3876226f..21b9229b5 100644 --- a/crates/federate/src/lib.rs +++ b/crates/federate/src/lib.rs @@ -1,10 +1,7 @@ use crate::{util::CancellableTask, worker::InstanceWorker}; use activitypub_federation::config::FederationConfig; use lemmy_api_common::context::LemmyContext; -use lemmy_db_schema::{ - newtypes::InstanceId, - source::{federation_queue_state::FederationQueueState, instance::Instance}, -}; +use lemmy_db_schema::{newtypes::InstanceId, source::instance::Instance}; use lemmy_utils::error::LemmyResult; use stats::receive_print_stats; use std::{collections::HashMap, time::Duration}; @@ -15,6 +12,7 @@ use tokio::{ }; use tokio_util::sync::CancellationToken; use tracing::info; +use util::FederationQueueStateWithDomain; mod stats; mod util; @@ -38,12 +36,12 @@ pub struct SendManager { opts: Opts, workers: HashMap, context: FederationConfig, - stats_sender: UnboundedSender<(InstanceId, FederationQueueState)>, + stats_sender: UnboundedSender, exit_print: JoinHandle<()>, } impl SendManager { - pub fn new(opts: Opts, context: FederationConfig) -> Self { + fn new(opts: Opts, context: FederationConfig) -> Self { assert!(opts.process_count > 0); assert!(opts.process_index > 0); assert!(opts.process_index <= opts.process_count); @@ -61,11 +59,27 @@ impl SendManager { } } - pub fn run(mut self) -> CancellableTask { - CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |cancel| async move { - self.do_loop(cancel).await?; - self.cancel().await?; - Ok(()) + pub fn run(opts: Opts, context: FederationConfig) -> CancellableTask { + CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |cancel| { + let opts = opts.clone(); + let context = context.clone(); + let mut manager = Self::new(opts, context); + async move { + let result = manager.do_loop(cancel).await; + // the loop function will only return if there is (a) an internal error (e.g. db connection + // failure) or (b) it was cancelled from outside. + if let Err(e) = result { + // don't let this error bubble up, just log it, so the below cancel function will run + // regardless + tracing::error!("SendManager failed: {e}"); + } + // cancel all the dependent workers as well to ensure they don't get orphaned and keep + // running. + manager.cancel().await?; + LemmyResult::Ok(()) + // if the task was not intentionally cancelled, then this whole lambda will be run again by + // CancellableTask after this + } }) } @@ -104,14 +118,24 @@ impl SendManager { continue; } // create new worker - let instance = instance.clone(); - let req_data = self.context.to_request_data(); + let context = self.context.clone(); let stats_sender = self.stats_sender.clone(); self.workers.insert( instance.id, - CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| async move { - InstanceWorker::init_and_loop(instance, req_data, stop, stats_sender).await?; - Ok(()) + CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| { + // if the instance worker ends unexpectedly due to internal/db errors, this lambda is rerun by cancellabletask. + let instance = instance.clone(); + let req_data = context.to_request_data(); + let stats_sender = stats_sender.clone(); + async move { + InstanceWorker::init_and_loop( + instance, + req_data, + stop, + stats_sender, + ) + .await + } }), ); } else if !should_federate { @@ -171,7 +195,7 @@ mod test { collections::HashSet, sync::{Arc, Mutex}, }; - use tokio::{spawn, time::sleep}; + use tokio::spawn; struct TestData { send_manager: SendManager, diff --git a/crates/federate/src/stats.rs b/crates/federate/src/stats.rs index bb6510263..f927f6ddf 100644 --- a/crates/federate/src/stats.rs +++ b/crates/federate/src/stats.rs @@ -1,15 +1,11 @@ -use crate::util::get_latest_activity_id; +use crate::util::{get_latest_activity_id, FederationQueueStateWithDomain}; use chrono::Local; -use diesel::result::Error::NotFound; use lemmy_api_common::federate_retry_sleep_duration; use lemmy_db_schema::{ newtypes::InstanceId, - source::{federation_queue_state::FederationQueueState, instance::Instance}, utils::{ActualDbPool, DbPool}, }; -use lemmy_utils::{error::LemmyResult, CACHE_DURATION_FEDERATION}; -use moka::future::Cache; -use once_cell::sync::Lazy; +use lemmy_utils::error::LemmyResult; use std::{collections::HashMap, time::Duration}; use tokio::{sync::mpsc::UnboundedReceiver, time::interval}; use tracing::{debug, info, warn}; @@ -18,7 +14,7 @@ use tracing::{debug, info, warn}; /// dropped) pub(crate) async fn receive_print_stats( pool: ActualDbPool, - mut receiver: UnboundedReceiver<(InstanceId, FederationQueueState)>, + mut receiver: UnboundedReceiver, ) { let pool = &mut DbPool::Pool(&pool); let mut printerval = interval(Duration::from_secs(60)); @@ -28,7 +24,7 @@ pub(crate) async fn receive_print_stats( ele = receiver.recv() => { match ele { // update stats for instance - Some((instance_id, ele)) => {stats.insert(instance_id, ele);}, + Some(ele) => {stats.insert(ele.state.instance_id, ele);}, // receiver closed, print stats and exit None => { print_stats(pool, &stats).await; @@ -43,7 +39,10 @@ pub(crate) async fn receive_print_stats( } } -async fn print_stats(pool: &mut DbPool<'_>, stats: &HashMap) { +async fn print_stats( + pool: &mut DbPool<'_>, + stats: &HashMap, +) { let res = print_stats_with_error(pool, stats).await; if let Err(e) = res { warn!("Failed to print stats: {e}"); @@ -52,18 +51,8 @@ async fn print_stats(pool: &mut DbPool<'_>, stats: &HashMap, - stats: &HashMap, + stats: &HashMap, ) -> LemmyResult<()> { - static INSTANCE_CACHE: Lazy>> = Lazy::new(|| { - Cache::builder() - .max_capacity(1) - .time_to_live(CACHE_DURATION_FEDERATION) - .build() - }); - let instances = INSTANCE_CACHE - .try_get_with((), async { Instance::read_all(pool).await }) - .await?; - let last_id = get_latest_activity_id(pool).await?; // it's expected that the values are a bit out of date, everything < SAVE_STATE_EVERY should be @@ -72,12 +61,9 @@ async fn print_stats_with_error( // todo: more stats (act/sec, avg http req duration) let mut ok_count = 0; let mut behind_count = 0; - for (instance_id, stat) in stats { - let domain = &instances - .iter() - .find(|i| &i.id == instance_id) - .ok_or(NotFound)? - .domain; + for ele in stats.values() { + let stat = &ele.state; + let domain = &ele.domain; let behind = last_id.0 - stat.last_successful_id.map(|e| e.0).unwrap_or(0); if stat.fail_count > 0 { info!( diff --git a/crates/federate/src/util.rs b/crates/federate/src/util.rs index 02a90dee9..60361c3c9 100644 --- a/crates/federate/src/util.rs +++ b/crates/federate/src/util.rs @@ -11,13 +11,13 @@ use lemmy_db_schema::{ source::{ activity::{ActorType, SentActivity}, community::Community, + federation_queue_state::FederationQueueState, person::Person, site::Site, }, traits::ApubActor, utils::{get_conn, DbPool}, }; -use lemmy_utils::error::LemmyResult; use moka::future::Cache; use once_cell::sync::Lazy; use reqwest::Url; @@ -25,7 +25,6 @@ use serde_json::Value; use std::{fmt::Debug, future::Future, pin::Pin, sync::Arc, time::Duration}; use tokio::{task::JoinHandle, time::sleep}; use tokio_util::sync::CancellationToken; -use tracing::error; /// Decrease the delays of the federation queue. /// Should only be used for federation tests since it significantly increases CPU and DB load of the @@ -59,26 +58,33 @@ pub struct CancellableTask { impl CancellableTask { /// spawn a task but with graceful shutdown - pub fn spawn( + pub fn spawn( timeout: Duration, - task: impl FnOnce(CancellationToken) -> F + Send + 'static, + task: impl Fn(CancellationToken) -> F + Send + 'static, ) -> CancellableTask where - F: Future> + Send + 'static, - R: Send + 'static, + F: Future + Send + 'static, + R: Send + Debug + 'static, { let stop = CancellationToken::new(); let stop2 = stop.clone(); - let task: JoinHandle> = tokio::spawn(task(stop2)); + let task: JoinHandle<()> = tokio::spawn(async move { + loop { + let res = task(stop2.clone()).await; + if stop2.is_cancelled() { + return; + } else { + tracing::warn!("task exited, restarting: {res:?}"); + } + } + }); let abort = task.abort_handle(); CancellableTask { f: Box::pin(async move { stop.cancel(); tokio::select! { r = task => { - if let Err(ref e) = r? { - error!("CancellableTask threw error: {e}"); - } + r.context("CancellableTask failed to cancel cleanly, returned error")?; Ok(()) }, _ = sleep(timeout) => { @@ -183,3 +189,10 @@ pub(crate) async fn get_latest_activity_id(pool: &mut DbPool<'_>) -> Result>, stop: CancellationToken, context: Data, - stats_sender: UnboundedSender<(InstanceId, FederationQueueState)>, + stats_sender: UnboundedSender, last_full_communities_fetch: DateTime, last_incremental_communities_fetch: DateTime, state: FederationQueueState, @@ -87,7 +88,7 @@ impl InstanceWorker { instance: Instance, context: Data, stop: CancellationToken, - stats_sender: UnboundedSender<(InstanceId, FederationQueueState)>, + stats_sender: UnboundedSender, ) -> Result<(), anyhow::Error> { let mut pool = context.pool(); let state = FederationQueueState::load(&mut pool, instance.id).await?; @@ -166,6 +167,14 @@ impl InstanceWorker { latest_id }; if id >= latest_id { + if id > latest_id { + tracing::error!( + "{}: last successful id {} is higher than latest id {} in database (did the db get cleared?)", + self.instance.domain, + id.0, + latest_id.0 + ); + } // no more work to be done, wait before rechecking tokio::select! { () = sleep(*WORK_FINISHED_RECHECK_DELAY) => {}, @@ -350,9 +359,10 @@ impl InstanceWorker { async fn save_and_send_state(&mut self) -> Result<()> { self.last_state_insert = Utc::now(); FederationQueueState::upsert(&mut self.context.pool(), &self.state).await?; - self - .stats_sender - .send((self.instance.id, self.state.clone()))?; + self.stats_sender.send(FederationQueueStateWithDomain { + state: self.state.clone(), + domain: self.instance.domain.clone(), + })?; Ok(()) } } diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 5e3db357a..f08f28c4a 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -355,7 +355,7 @@ async fn get_feed_front( let posts = PostQuery { listing_type: (Some(ListingType::Subscribed)), - local_user: (Some(&local_user)), + local_user: (Some(&local_user.local_user)), sort: (Some(*sort_type)), limit: (Some(*limit)), page: (Some(*page)), diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 96d7d317c..049bd6cc8 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -103,7 +103,13 @@ async fn upload( pictrs_alias: image.file.to_string(), pictrs_delete_token: image.delete_token.to_string(), }; - LocalImage::create(&mut context.pool(), &form).await?; + + let protocol_and_hostname = context.settings().get_protocol_and_hostname(); + let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?; + + // Also store the details for the image + let details_form = image.details.build_image_details_form(&thumbnail_url); + LocalImage::create(&mut context.pool(), &form, &details_form).await?; } } diff --git a/crates/routes/src/nodeinfo.rs b/crates/routes/src/nodeinfo.rs index f64bb72c5..bcb835309 100644 --- a/crates/routes/src/nodeinfo.rs +++ b/crates/routes/src/nodeinfo.rs @@ -9,17 +9,21 @@ use lemmy_utils::{ VERSION, }; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use url::Url; +/// A description of the nodeinfo endpoint is here: +/// https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md pub fn config(cfg: &mut web::ServiceConfig) { cfg .route( - "/nodeinfo/2.1.json", + "/nodeinfo/2.1", web::get().to(node_info).wrap(cache_1hour()), ) - .service(web::redirect("/version", "/nodeinfo/2.1.json")) + .service(web::redirect("/version", "/nodeinfo/2.1")) // For backwards compatibility, can be removed after Lemmy 0.20 - .service(web::redirect("/nodeinfo/2.0.json", "/nodeinfo/2.1.json")) + .service(web::redirect("/nodeinfo/2.0.json", "/nodeinfo/2.1")) + .service(web::redirect("/nodeinfo/2.1.json", "/nodeinfo/2.1")) .route( "/.well-known/nodeinfo", web::get().to(node_info_well_known).wrap(cache_3days()), @@ -31,7 +35,7 @@ async fn node_info_well_known(context: web::Data) -> LemmyResult) -> Result, } #[derive(Serialize, Deserialize, Debug)] -struct NodeInfoWellKnownLinks { +pub struct NodeInfoWellKnownLinks { pub rel: Url, pub href: Url, } @@ -99,7 +103,7 @@ pub struct NodeInfo { pub open_registrations: Option, /// These fields are required by the spec for no reason pub services: Option, - pub metadata: Option>, + pub metadata: Option>, } #[derive(Serialize, Deserialize, Debug, Default)] diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index a913e6243..8891411a5 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -11,8 +11,10 @@ static VALID_MATRIX_ID_REGEX: Lazy = Lazy::new(|| { }); // taken from https://en.wikipedia.org/wiki/UTM_parameters static CLEAN_URL_PARAMS_REGEX: Lazy = Lazy::new(|| { - Regex::new(r"^utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|gclsrc|dclid|fbclid$") - .expect("compile regex") + Regex::new( + r"^(utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|gclsrc|dclid|fbclid)=", + ) + .expect("compile regex") }); const ALLOWED_POST_URL_SCHEMES: [&str; 3] = ["http", "https", "magnet"]; @@ -158,14 +160,12 @@ pub fn is_valid_post_title(title: &str) -> LemmyResult<()> { } /// This could be post bodies, comments, or any description field -pub fn is_valid_body_field(body: &Option, post: bool) -> LemmyResult<()> { - if let Some(body) = body { - if post { - max_length_check(body, POST_BODY_MAX_LENGTH, LemmyErrorType::InvalidBodyField)?; - } else { - max_length_check(body, BODY_MAX_LENGTH, LemmyErrorType::InvalidBodyField)?; - }; - } +pub fn is_valid_body_field(body: &str, post: bool) -> LemmyResult<()> { + if post { + max_length_check(body, POST_BODY_MAX_LENGTH, LemmyErrorType::InvalidBodyField)?; + } else { + max_length_check(body, BODY_MAX_LENGTH, LemmyErrorType::InvalidBodyField)?; + }; Ok(()) } @@ -173,16 +173,14 @@ pub fn is_valid_bio_field(bio: &str) -> LemmyResult<()> { max_length_check(bio, BIO_MAX_LENGTH, LemmyErrorType::BioLengthOverflow) } -pub fn is_valid_alt_text_field(alt_text: &Option) -> LemmyResult<()> { - if let Some(alt_text) = alt_text { - max_length_check( - alt_text, - ALT_TEXT_MAX_LENGTH, - LemmyErrorType::AltTextLengthOverflow, - ) - } else { - Ok(()) - } +pub fn is_valid_alt_text_field(alt_text: &str) -> LemmyResult<()> { + max_length_check( + alt_text, + ALT_TEXT_MAX_LENGTH, + LemmyErrorType::AltTextLengthOverflow, + )?; + + Ok(()) } /// Checks the site name length, the limit as defined in the DB. @@ -260,12 +258,11 @@ pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> LemmyResult