diff --git a/.woodpecker.yml b/.woodpecker.yml index 55f0ec8b2..cb6580e32 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -42,14 +42,14 @@ steps: - event: [pull_request, tag] prettier_check: - image: tmknom/prettier:3.0.0 + image: tmknom/prettier:3.2.5 commands: - prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml' when: - event: pull_request toml_fmt: - image: tamasfe/taplo:0.8.1 + image: tamasfe/taplo:0.9.3 commands: - taplo format --check when: diff --git a/Cargo.lock b/Cargo.lock index a799d9d6f..c38a00d0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,7 @@ dependencies = [ "pin-project-lite", "rand", "regex", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "rsa", "serde", @@ -298,9 +298,9 @@ dependencies = [ [[package]] name = "actix-web-prom" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76743e67d4e7efa9fc2ac7123de0dd7b2ca592668e19334f1d81a3b077afc6ac" +checksum = "56a34f1825c3ae06567a9d632466809bbf34963c86002e8921b64f32d48d289d" dependencies = [ "actix-web", "futures-core", @@ -839,9 +839,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" dependencies = [ "clap_builder", "clap_derive", @@ -849,9 +849,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" dependencies = [ "anstream", "anstyle", @@ -861,9 +861,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1645,9 +1645,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1660,9 +1660,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1670,15 +1670,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1687,15 +1687,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1704,21 +1704,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1875,9 +1875,9 @@ dependencies = [ [[package]] name = "html2text" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c66ee488a63a92237d5b48875b7e05bb293be8fb2894641c8118b60c08ab5ef" +checksum = "042a9677c258ac2952dd026bb0cd21972f00f644a5a38f5a215cb22cdaf6834e" dependencies = [ "html5ever 0.27.0", "markup5ever 0.12.1", @@ -1989,7 +1989,7 @@ dependencies = [ "base64 0.22.1", "http-signature-normalization", "httpdate", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "sha2", "thiserror", @@ -2075,7 +2075,7 @@ dependencies = [ "http 1.1.0", "hyper 1.4.1", "hyper-util", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -2514,7 +2514,7 @@ dependencies = [ "moka", "pretty_assertions", "regex", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "rosetta-i18n", "serde", @@ -2579,7 +2579,7 @@ dependencies = [ "lemmy_utils", "moka", "pretty_assertions", - "reqwest 0.12.7", + "reqwest 0.12.8", "serde", "serde_json", "serde_with", @@ -2630,7 +2630,7 @@ dependencies = [ "moka", "pretty_assertions", "regex", - "rustls 0.23.13", + "rustls 0.23.14", "serde", "serde_json", "serde_with", @@ -2718,7 +2718,7 @@ dependencies = [ "lemmy_utils", "mockall", "moka", - "reqwest 0.12.7", + "reqwest 0.12.8", "serde_json", "serial_test", "test-context", @@ -2745,7 +2745,7 @@ dependencies = [ "lemmy_db_views", "lemmy_db_views_actor", "lemmy_utils", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "rss", "serde", @@ -2778,10 +2778,10 @@ dependencies = [ "lemmy_utils", "pretty_assertions", "prometheus", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "reqwest-tracing", - "rustls 0.23.13", + "rustls 0.23.14", "serde_json", "serial_test", "tokio", @@ -2811,7 +2811,7 @@ dependencies = [ "markdown-it", "pretty_assertions", "regex", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "rosetta-build", "rosetta-i18n", @@ -2847,7 +2847,7 @@ dependencies = [ "nom", "percent-encoding", "quoted_printable", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pemfile 2.1.3", "rustls-pki-types", "socket2", @@ -3762,7 +3762,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.14", "socket2", "thiserror", "tokio", @@ -3779,7 +3779,7 @@ dependencies = [ "rand", "ring", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.14", "slab", "thiserror", "tinyvec", @@ -3875,14 +3875,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -3896,13 +3896,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -3919,9 +3919,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -3966,9 +3966,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ "async-compression", "base64 0.22.1", @@ -3990,7 +3990,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pemfile 2.1.3", "rustls-pki-types", "serde", @@ -4019,7 +4019,7 @@ dependencies = [ "anyhow", "async-trait", "http 1.1.0", - "reqwest 0.12.7", + "reqwest 0.12.8", "serde", "thiserror", "tower-service", @@ -4036,7 +4036,7 @@ dependencies = [ "getrandom", "http 1.1.0", "matchit", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "tracing", ] @@ -4177,9 +4177,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "aws-lc-rs", "log", @@ -4212,9 +4212,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" @@ -4373,9 +4373,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ "base64 0.22.1", "chrono", @@ -4391,9 +4391,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling 0.20.10", "proc-macro2", @@ -4737,7 +4737,7 @@ dependencies = [ "fnv", "once_cell", "plist", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "serde", "serde_derive", "serde_json", @@ -4974,7 +4974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" dependencies = [ "ring", - "rustls 0.23.13", + "rustls 0.23.14", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", @@ -4997,7 +4997,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pki-types", "tokio", ] @@ -5579,7 +5579,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 49a8e1d61..fc22d0d53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,7 @@ lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" } lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" } lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" } lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" } -activitypub_federation = { version = "0.6.0-alpha1", default-features = false, features = [ +activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [ "actix-web", ] } diesel = "2.1.6" @@ -188,7 +188,7 @@ chrono = { workspace = true } prometheus = { version = "0.13.4", features = ["process"] } serial_test = { workspace = true } clap = { workspace = true } -actix-web-prom = "0.8.0" +actix-web-prom = "0.9.0" [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/api_tests/package.json b/api_tests/package.json index b06b84fbf..63c212d01 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -6,7 +6,7 @@ "repository": "https://github.com/LemmyNet/lemmy", "author": "Dessalines", "license": "AGPL-3.0", - "packageManager": "pnpm@9.9.0", + "packageManager": "pnpm@9.12.0", "scripts": { "lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'", "fix": "prettier --write src && eslint --fix src", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 54c8a9d96..6807f43f8 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -10,25 +10,25 @@ importers: devDependencies: '@types/jest': specifier: ^29.5.12 - version: 29.5.12 + version: 29.5.13 '@types/node': specifier: ^22.3.0 - version: 22.3.0 + version: 22.7.4 '@typescript-eslint/eslint-plugin': specifier: ^8.1.0 - version: 8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4) + version: 8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.2))(eslint@9.12.0)(typescript@5.6.2) '@typescript-eslint/parser': specifier: ^8.1.0 - version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) + version: 8.8.1(eslint@9.12.0)(typescript@5.6.2) eslint: specifier: ^9.9.0 - version: 9.9.0 + version: 9.12.0 eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.2.1(eslint@9.9.0)(prettier@3.3.3) + version: 5.2.1(eslint@9.12.0)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@22.3.0) + version: 29.7.0(@types/node@22.7.4) lemmy-js-client: specifier: 0.20.0-alpha.11 version: 0.20.0-alpha.11 @@ -37,13 +37,13 @@ importers: version: 3.3.3 ts-jest: specifier: ^29.1.0 - version: 29.2.4(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.3.0))(typescript@5.5.4) + version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.7.4))(typescript@5.6.2) typescript: specifier: ^5.5.4 - version: 5.5.4 + version: 5.6.2 typescript-eslint: specifier: ^8.1.0 - version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) + version: 8.8.1(eslint@9.12.0)(typescript@5.6.2) packages: @@ -51,8 +51,8 @@ packages: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} - '@babel/code-frame@7.23.5': - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.23.5': @@ -113,6 +113,10 @@ packages: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.23.5': resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} @@ -121,8 +125,8 @@ packages: resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} engines: {node: '>=6.9.0'} - '@babel/highlight@7.23.4': - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} '@babel/parser@7.23.9': @@ -224,32 +228,48 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + '@eslint-community/regexpp@4.11.1': + resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.17.1': - resolution: {integrity: sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==} + '@eslint/config-array@0.18.0': + resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.6.0': + resolution: {integrity: sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.1.0': resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.9.0': - resolution: {integrity: sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==} + '@eslint/js@9.12.0': + resolution: {integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==} 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} + '@eslint/plugin-kit@0.2.0': + resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.0': + resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.5': + resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} + engines: {node: '>=18.18.0'} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.0': - resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} '@istanbuljs/load-nyc-config@1.1.0': @@ -381,6 +401,9 @@ packages: '@types/babel__traverse@7.20.5': resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -393,11 +416,14 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@29.5.12': - resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + '@types/jest@29.5.13': + resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==} - '@types/node@22.3.0': - resolution: {integrity: sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.7.4': + resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -408,8 +434,8 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@8.1.0': - resolution: {integrity: sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw==} + '@typescript-eslint/eslint-plugin@8.8.1': + resolution: {integrity: sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -419,8 +445,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.1.0': - resolution: {integrity: sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA==} + '@typescript-eslint/parser@8.8.1': + resolution: {integrity: sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -429,12 +455,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.1.0': - resolution: {integrity: sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ==} + '@typescript-eslint/scope-manager@8.8.1': + resolution: {integrity: sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.1.0': - resolution: {integrity: sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA==} + '@typescript-eslint/type-utils@8.8.1': + resolution: {integrity: sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -442,12 +468,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.1.0': - resolution: {integrity: sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog==} + '@typescript-eslint/types@8.8.1': + resolution: {integrity: sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.1.0': - resolution: {integrity: sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg==} + '@typescript-eslint/typescript-estree@8.8.1': + resolution: {integrity: sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -455,14 +481,14 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.1.0': - resolution: {integrity: sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA==} + '@typescript-eslint/utils@8.8.1': + resolution: {integrity: sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@typescript-eslint/visitor-keys@8.1.0': - resolution: {integrity: sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag==} + '@typescript-eslint/visitor-keys@8.8.1': + resolution: {integrity: sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -508,12 +534,8 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} @@ -645,8 +667,8 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} - debug@4.3.6: - resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -677,10 +699,6 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -729,20 +747,20 @@ packages: eslint-config-prettier: optional: true - eslint-scope@8.0.2: - resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + eslint-scope@8.1.0: + resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} 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-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + eslint-visitor-keys@4.1.0: + resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.9.0: - resolution: {integrity: sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==} + eslint@9.12.0: + resolution: {integrity: sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -751,8 +769,8 @@ packages: jiti: optional: true - espree@10.1.0: - resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + espree@10.2.0: + resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: @@ -887,10 +905,6 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -966,10 +980,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1227,8 +1237,8 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} mimic-fn@2.1.0: @@ -1246,8 +1256,8 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1320,12 +1330,8 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -1370,8 +1376,8 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -1518,8 +1524,8 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-jest@29.2.4: - resolution: {integrity: sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==} + ts-jest@29.2.5: + resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -1557,8 +1563,8 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript-eslint@8.1.0: - resolution: {integrity: sha512-prB2U3jXPJLpo1iVLN338Lvolh6OrcCZO+9Yv6AR+tvegPPptYCDBIHiEEUdqRi8gAv2bXNKfMUrgAd2ejn/ow==} + typescript-eslint@8.8.1: + resolution: {integrity: sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1566,13 +1572,13 @@ packages: typescript: optional: true - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.18.2: - resolution: {integrity: sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} update-browserslist-db@1.0.13: resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} @@ -1636,17 +1642,17 @@ snapshots: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.22 - '@babel/code-frame@7.23.5': + '@babel/code-frame@7.24.7': dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 + '@babel/highlight': 7.24.7 + picocolors: 1.1.0 '@babel/compat-data@7.23.5': {} '@babel/core@7.23.9': dependencies: '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/generator': 7.23.6 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) @@ -1656,7 +1662,7 @@ snapshots: '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 convert-source-map: 2.0.0 - debug: 4.3.6 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1716,6 +1722,8 @@ snapshots: '@babel/helper-validator-identifier@7.22.20': {} + '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-validator-option@7.23.5': {} '@babel/helpers@7.23.9': @@ -1726,11 +1734,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/highlight@7.23.4': + '@babel/highlight@7.24.7': dependencies: - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 + picocolors: 1.1.0 '@babel/parser@7.23.9': dependencies: @@ -1808,13 +1817,13 @@ snapshots: '@babel/template@7.23.9': dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 '@babel/traverse@7.23.9': dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/generator': 7.23.6 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 @@ -1822,7 +1831,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - debug: 4.3.6 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1835,26 +1844,28 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.0(eslint@9.9.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.12.0)': dependencies: - eslint: 9.9.0 + eslint: 9.12.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.0': {} + '@eslint-community/regexpp@4.11.1': {} - '@eslint/config-array@0.17.1': + '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.6 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color + '@eslint/core@0.6.0': {} + '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.6 - espree: 10.1.0 + debug: 4.3.7 + espree: 10.2.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 @@ -1864,13 +1875,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.9.0': {} + '@eslint/js@9.12.0': {} '@eslint/object-schema@2.1.4': {} + '@eslint/plugin-kit@0.2.0': + dependencies: + levn: 0.4.1 + + '@humanfs/core@0.19.0': {} + + '@humanfs/node@0.16.5': + dependencies: + '@humanfs/core': 0.19.0 + '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.0': {} + '@humanwhocodes/retry@0.3.1': {} '@istanbuljs/load-nyc-config@1.1.0': dependencies: @@ -1885,7 +1907,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1898,14 +1920,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.3.0) + jest-config: 29.7.0(@types/node@22.7.4) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1930,7 +1952,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1948,7 +1970,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.3.0 + '@types/node': 22.7.4 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1970,7 +1992,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 22.3.0 + '@types/node': 22.7.4 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2040,7 +2062,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.3.0 + '@types/node': 22.7.4 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2106,9 +2128,11 @@ snapshots: dependencies: '@babel/types': 7.23.9 + '@types/estree@1.0.6': {} + '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.7.4 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2120,14 +2144,16 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@29.5.12': + '@types/jest@29.5.13': dependencies: expect: 29.7.0 pretty-format: 29.7.0 - '@types/node@22.3.0': + '@types/json-schema@7.0.15': {} + + '@types/node@22.7.4': dependencies: - undici-types: 6.18.2 + undici-types: 6.19.8 '@types/stack-utils@2.0.3': {} @@ -2137,85 +2163,85 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.2))(eslint@9.12.0)(typescript@5.6.2)': dependencies: - '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.1.0(eslint@9.9.0)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.1.0 - '@typescript-eslint/type-utils': 8.1.0(eslint@9.9.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.1.0(eslint@9.9.0)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.1.0 - eslint: 9.9.0 + '@eslint-community/regexpp': 4.11.1 + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.2) + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/type-utils': 8.8.1(eslint@9.12.0)(typescript@5.6.2) + '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.8.1 + eslint: 9.12.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.6.2) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.1.0(eslint@9.9.0)(typescript@5.5.4)': + '@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.2)': dependencies: - '@typescript-eslint/scope-manager': 8.1.0 - '@typescript-eslint/types': 8.1.0 - '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.1.0 - debug: 4.3.6 - eslint: 9.9.0 + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.8.1 + debug: 4.3.7 + eslint: 9.12.0 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.1.0': + '@typescript-eslint/scope-manager@8.8.1': dependencies: - '@typescript-eslint/types': 8.1.0 - '@typescript-eslint/visitor-keys': 8.1.0 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/visitor-keys': 8.8.1 - '@typescript-eslint/type-utils@8.1.0(eslint@9.9.0)(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.8.1(eslint@9.12.0)(typescript@5.6.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.1.0(eslint@9.9.0)(typescript@5.5.4) - debug: 4.3.6 - ts-api-utils: 1.3.0(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.2) + '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.2) + debug: 4.3.7 + ts-api-utils: 1.3.0(typescript@5.6.2) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 transitivePeerDependencies: - eslint - supports-color - '@typescript-eslint/types@8.1.0': {} + '@typescript-eslint/types@8.8.1': {} - '@typescript-eslint/typescript-estree@8.1.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.8.1(typescript@5.6.2)': dependencies: - '@typescript-eslint/types': 8.1.0 - '@typescript-eslint/visitor-keys': 8.1.0 - debug: 4.3.6 - globby: 11.1.0 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/visitor-keys': 8.8.1 + debug: 4.3.7 + fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.6.2) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.1.0(eslint@9.9.0)(typescript@5.5.4)': + '@typescript-eslint/utils@8.8.1(eslint@9.12.0)(typescript@5.6.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) - '@typescript-eslint/scope-manager': 8.1.0 - '@typescript-eslint/types': 8.1.0 - '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) - eslint: 9.9.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.2) + eslint: 9.12.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@8.1.0': + '@typescript-eslint/visitor-keys@8.8.1': dependencies: - '@typescript-eslint/types': 8.1.0 + '@typescript-eslint/types': 8.8.1 eslint-visitor-keys: 3.4.3 acorn-jsx@5.3.2(acorn@8.12.1): @@ -2258,9 +2284,7 @@ snapshots: argparse@2.0.1: {} - array-union@2.1.0: {} - - async@3.2.5: {} + async@3.2.6: {} babel-jest@29.7.0(@babel/core@7.23.9): dependencies: @@ -2401,13 +2425,13 @@ snapshots: convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@22.3.0): + create-jest@29.7.0(@types/node@22.7.4): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.3.0) + jest-config: 29.7.0(@types/node@22.7.4) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2422,9 +2446,9 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.3.6: + debug@4.3.7: dependencies: - ms: 2.1.2 + ms: 2.1.3 dedent@1.5.1: {} @@ -2436,10 +2460,6 @@ snapshots: diff-sequences@29.6.3: {} - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - ejs@3.1.10: dependencies: jake: 10.9.2 @@ -2462,40 +2482,44 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.2.1(eslint@9.9.0)(prettier@3.3.3): + eslint-plugin-prettier@5.2.1(eslint@9.12.0)(prettier@3.3.3): dependencies: - eslint: 9.9.0 + eslint: 9.12.0 prettier: 3.3.3 prettier-linter-helpers: 1.0.0 synckit: 0.9.1 - eslint-scope@8.0.2: + eslint-scope@8.1.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.0.0: {} + eslint-visitor-keys@4.1.0: {} - eslint@9.9.0: + eslint@9.12.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) - '@eslint-community/regexpp': 4.11.0 - '@eslint/config-array': 0.17.1 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) + '@eslint-community/regexpp': 4.11.1 + '@eslint/config-array': 0.18.0 + '@eslint/core': 0.6.0 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.9.0 + '@eslint/js': 9.12.0 + '@eslint/plugin-kit': 0.2.0 + '@humanfs/node': 0.16.5 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.0 - '@nodelib/fs.walk': 1.2.8 + '@humanwhocodes/retry': 0.3.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6 + debug: 4.3.7 escape-string-regexp: 4.0.0 - eslint-scope: 8.0.2 - eslint-visitor-keys: 4.0.0 - espree: 10.1.0 + eslint-scope: 8.1.0 + eslint-visitor-keys: 4.1.0 + espree: 10.2.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -2505,23 +2529,20 @@ snapshots: ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color - espree@10.1.0: + espree@10.2.0: dependencies: acorn: 8.12.1 acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.0.0 + eslint-visitor-keys: 4.1.0 esprima@4.0.1: {} @@ -2569,7 +2590,7 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.7 + micromatch: 4.0.8 fast-json-stable-stringify@2.1.0: {} @@ -2652,15 +2673,6 @@ snapshots: globals@14.0.0: {} - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -2716,8 +2728,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-stream@2.0.1: {} isexe@2.0.0: {} @@ -2752,7 +2762,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.6 + debug: 4.3.7 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -2765,7 +2775,7 @@ snapshots: jake@10.9.2: dependencies: - async: 3.2.5 + async: 3.2.6 chalk: 4.1.2 filelist: 1.0.4 minimatch: 3.1.2 @@ -2782,7 +2792,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -2802,16 +2812,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.3.0): + jest-cli@29.7.0(@types/node@22.7.4): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.3.0) + create-jest: 29.7.0(@types/node@22.7.4) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.3.0) + jest-config: 29.7.0(@types/node@22.7.4) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2821,7 +2831,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.3.0): + jest-config@29.7.0(@types/node@22.7.4): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 @@ -2846,7 +2856,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.3.0 + '@types/node': 22.7.4 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2875,7 +2885,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2885,7 +2895,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.3.0 + '@types/node': 22.7.4 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2911,12 +2921,12 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 @@ -2924,7 +2934,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2959,7 +2969,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2987,7 +2997,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -3033,7 +3043,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3052,7 +3062,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 22.7.4 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3061,17 +3071,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.3.0 + '@types/node': 22.7.4 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.3.0): + jest@29.7.0(@types/node@22.7.4): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.3.0) + jest-cli: 29.7.0(@types/node@22.7.4) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3153,7 +3163,7 @@ snapshots: braces: 3.0.2 picomatch: 2.3.1 - micromatch@4.0.7: + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 @@ -3172,7 +3182,7 @@ snapshots: dependencies: brace-expansion: 2.0.1 - ms@2.1.2: {} + ms@2.1.3: {} natural-compare@1.4.0: {} @@ -3227,7 +3237,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -3240,9 +3250,7 @@ snapshots: path-parse@1.0.7: {} - path-type@4.0.0: {} - - picocolors@1.0.0: {} + picocolors@1.1.0: {} picomatch@2.3.1: {} @@ -3264,7 +3272,7 @@ snapshots: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 18.2.0 + react-is: 18.3.1 prompts@2.4.2: dependencies: @@ -3277,7 +3285,7 @@ snapshots: queue-microtask@1.2.3: {} - react-is@18.2.0: {} + react-is@18.3.1: {} require-directory@2.1.1: {} @@ -3390,22 +3398,22 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@1.3.0(typescript@5.5.4): + ts-api-utils@1.3.0(typescript@5.6.2): dependencies: - typescript: 5.5.4 + typescript: 5.6.2 - ts-jest@29.2.4(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.3.0))(typescript@5.5.4): + ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.7.4))(typescript@5.6.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.3.0) + jest: 29.7.0(@types/node@22.7.4) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.6.3 - typescript: 5.5.4 + typescript: 5.6.2 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.23.9 @@ -3423,26 +3431,26 @@ snapshots: type-fest@0.21.3: {} - typescript-eslint@8.1.0(eslint@9.9.0)(typescript@5.5.4): + typescript-eslint@8.8.1(eslint@9.12.0)(typescript@5.6.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4) - '@typescript-eslint/parser': 8.1.0(eslint@9.9.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.1.0(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.2))(eslint@9.12.0)(typescript@5.6.2) + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.2) + '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.2) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 transitivePeerDependencies: - eslint - supports-color - typescript@5.5.4: {} + typescript@5.6.2: {} - undici-types@6.18.2: {} + undici-types@6.19.8: {} update-browserslist-db@1.0.13(browserslist@4.22.3): dependencies: browserslist: 4.22.3 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.1.0 uri-js@4.4.1: dependencies: diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index c18469e33..5f2059e4f 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -858,3 +858,26 @@ test("Dont send a comment reply to a blocked community", async () => { blockRes = await blockCommunity(beta, newCommunityId, false); expect(blockRes.blocked).toBe(false); }); + +/// Fetching a deeply nested comment can lead to stack overflow as all parent comments are also +/// fetched recursively. Ensure that it works properly. +test("Fetch a deeply nested comment", async () => { + let lastComment; + for (let i = 0; i < 50; i++) { + let commentRes = await createComment( + alpha, + postOnAlphaRes.post_view.post.id, + lastComment?.comment_view.comment.id, + ); + expect(commentRes.comment_view.comment).toBeDefined(); + lastComment = commentRes; + } + + let betaComment = await resolveComment( + beta, + lastComment!.comment_view.comment, + ); + + expect(betaComment!.comment!.comment).toBeDefined(); + expect(betaComment?.comment?.post).toBeDefined(); +}); diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index ee9cc441d..59e3d774e 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -794,3 +794,29 @@ test("Fetch post with redirect", async () => { let gammaPost2 = await gamma.resolveObject(form); expect(gammaPost2.post).toBeDefined(); }); + +test("Rewrite markdown links", async () => { + const community = (await resolveBetaCommunity(beta)).community!; + + // create a post + let postRes1 = await createPost(beta, community.community.id); + + // link to this post in markdown + let postRes2 = await createPost( + beta, + community.community.id, + "https://example.com/", + `[link](${postRes1.post_view.post.ap_id})`, + ); + console.log(postRes2.post_view.post.body); + expect(postRes2.post_view.post).toBeDefined(); + + // fetch both posts from another instance + const alphaPost1 = await resolvePost(alpha, postRes1.post_view.post); + const alphaPost2 = await resolvePost(alpha, postRes2.post_view.post); + + // remote markdown link is replaced with local link + expect(alphaPost2.post?.post.body).toBe( + `[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`, + ); +}); diff --git a/config/defaults.hjson b/config/defaults.hjson index 4bce48b5f..f0b9d56df 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -75,6 +75,8 @@ "ProxyAllImages" # Timeout for uploading images to pictrs (in seconds) upload_timeout: 30 + # Resize post thumbnails to this maximum width/height. + max_thumbnail_size: 256 } # Email sending configuration. All options except login/password are mandatory email: { diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index 81d5516c1..749b90426 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ comment::{CommentResponse, CreateCommentLike}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_bot_account, check_community_user_action, check_downvotes_enabled}, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem}, }; use lemmy_db_schema::{ newtypes::LocalUserId, @@ -27,14 +27,20 @@ pub async fn like_comment( local_user_view: LocalUserView, ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; + let comment_id = data.comment_id; let mut recipient_ids = Vec::::new(); - // Don't do a downvote if site has downvotes disabled - check_downvotes_enabled(data.score, &local_site)?; + check_local_vote_mode( + data.score, + VoteItem::Comment(comment_id), + &local_site, + local_user_view.person.id, + &mut context.pool(), + ) + .await?; check_bot_account(&local_user_view.person)?; - let comment_id = data.comment_id; let orig_comment = CommentView::read( &mut context.pool(), comment_id, diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index f7da2154f..64b1c7196 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -92,8 +92,10 @@ pub async fn ban_from_community( let remove_data = data.ban; remove_or_restore_user_data_in_community( data.community_id, + local_user_view.person.id, banned_person_id, remove_data, + &data.reason, &mut context.pool(), ) .await?; diff --git a/crates/api/src/community/mod.rs b/crates/api/src/community/mod.rs index 478192229..54bdbef28 100644 --- a/crates/api/src/community/mod.rs +++ b/crates/api/src/community/mod.rs @@ -3,4 +3,5 @@ pub mod ban; pub mod block; pub mod follow; pub mod hide; +pub mod random; pub mod transfer; diff --git a/crates/api/src/community/random.rs b/crates/api/src/community/random.rs new file mode 100644 index 000000000..3cc04e126 --- /dev/null +++ b/crates/api/src/community/random.rs @@ -0,0 +1,55 @@ +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; +use lemmy_api_common::{ + community::{CommunityResponse, GetRandomCommunity}, + context::LemmyContext, + utils::{check_private_instance, is_mod_or_admin_opt}, +}; +use lemmy_db_schema::source::{ + actor_language::CommunityLanguage, + community::Community, + local_site::LocalSite, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn get_random_community( + data: Query, + context: Data, + local_user_view: Option, +) -> LemmyResult> { + let local_site = LocalSite::read(&mut context.pool()).await?; + + check_private_instance(&local_user_view, &local_site)?; + + let local_user = local_user_view.as_ref().map(|u| &u.local_user); + + let random_community_id = + Community::get_random_community_id(&mut context.pool(), &data.type_).await?; + + let is_mod_or_admin = is_mod_or_admin_opt( + &mut context.pool(), + local_user_view.as_ref(), + Some(random_community_id), + ) + .await + .is_ok(); + + let community_view = CommunityView::read( + &mut context.pool(), + random_community_id, + local_user, + is_mod_or_admin, + ) + .await?; + + let discussion_languages = + CommunityLanguage::read(&mut context.pool(), random_community_id).await?; + + Ok(Json(CommunityResponse { + community_view, + discussion_languages, + })) +} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 44a8d9bbc..6ffa52f77 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -265,8 +265,6 @@ pub async fn local_user_view_from_jwt( } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 7ab8ad72a..2ace7f031 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ context::LemmyContext, person::{BanPerson, BanPersonResponse}, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_expire_time, is_admin, remove_user_data, restore_user_data}, + utils::{check_expire_time, is_admin, remove_or_restore_user_data}, }; use lemmy_db_schema::{ source::{ @@ -66,11 +66,15 @@ pub async fn ban_from_site( // Remove their data if that's desired if data.remove_or_restore_data.unwrap_or(false) { - if data.ban { - remove_user_data(person.id, &context).await?; - } else { - restore_user_data(person.id, &context).await?; - } + let removed = data.ban; + remove_or_restore_user_data( + local_user_view.person.id, + person.id, + removed, + &data.reason, + &context, + ) + .await?; }; // Mod tables diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index 029914545..08820cadd 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -130,7 +130,6 @@ pub async fn save_user_settings( send_notifications_to_email: data.send_notifications_to_email, show_nsfw: data.show_nsfw, blur_nsfw: data.blur_nsfw, - auto_expand: data.auto_expand, show_bot_accounts: data.show_bot_accounts, default_post_sort_type, default_comment_sort_type, diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index 967b22a30..c81d9630a 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -8,8 +8,9 @@ use lemmy_api_common::{ utils::{ check_bot_account, check_community_user_action, - check_downvotes_enabled, + check_local_vote_mode, mark_post_as_read, + VoteItem, }, }; use lemmy_db_schema::{ @@ -31,13 +32,19 @@ pub async fn like_post( local_user_view: LocalUserView, ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; + let post_id = data.post_id; - // Don't do a downvote if site has downvotes disabled - check_downvotes_enabled(data.score, &local_site)?; + check_local_vote_mode( + data.score, + VoteItem::Post(post_id), + &local_site, + local_user_view.person.id, + &mut context.pool(), + ) + .await?; check_bot_account(&local_user_view.person)?; // Check for a community ban - let post_id = data.post_id; let post = Post::read(&mut context.pool(), post_id).await?; check_community_user_action( diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 5e1e69d49..97ad7e2e5 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -63,7 +63,7 @@ pub async fn leave_admin( let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; let oauth_providers = OAuthProvider::get_all_public(&mut context.pool()).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; - let tagline = Tagline::get_random(&mut context.pool()).await?; + let tagline = Tagline::get_random(&mut context.pool()).await.ok(); Ok(Json(GetSiteResponse { site_view, @@ -76,5 +76,7 @@ pub async fn leave_admin( admin_oauth_providers: None, blocked_urls, tagline, + taglines: vec![], + custom_emojis: vec![], })) } diff --git a/crates/api/src/site/registration_applications/tests.rs b/crates/api/src/site/registration_applications/tests.rs index 30cbdde72..022cbf236 100644 --- a/crates/api/src/site/registration_applications/tests.rs +++ b/crates/api/src/site/registration_applications/tests.rs @@ -34,13 +34,10 @@ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{error::LemmyResult, LemmyErrorType, CACHE_DURATION_API}; use serial_test::serial; -#[allow(clippy::unwrap_used)] async fn create_test_site(context: &Data) -> LemmyResult<(Instance, LocalUserView)> { let pool = &mut context.pool(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .expect("Create test instance"); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let admin_person = Person::create( pool, @@ -57,7 +54,7 @@ async fn create_test_site(context: &Data) -> LemmyResult<(Instance let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id).await?; let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id); - let site = Site::create(pool, &site_form).await.unwrap(); + let site = Site::create(pool, &site_form).await?; // Create a local site, since this is necessary for determining if email verification is // required @@ -68,14 +65,12 @@ async fn create_test_site(context: &Data) -> LemmyResult<(Instance site_setup: Some(true), ..LocalSiteInsertForm::new(site.id) }; - let local_site = LocalSite::create(pool, &local_site_form).await.unwrap(); + let local_site = LocalSite::create(pool, &local_site_form).await?; // Required to have a working local SiteView when updating the site to change email verification // requirement or registration mode let rate_limit_form = LocalSiteRateLimitInsertForm::new(local_site.id); - LocalSiteRateLimit::create(pool, &rate_limit_form) - .await - .unwrap(); + LocalSiteRateLimit::create(pool, &rate_limit_form).await?; Ok((inserted_instance, admin_local_user_view)) } @@ -109,7 +104,6 @@ async fn signup( Ok((local_user, application)) } -#[allow(clippy::unwrap_used)] async fn get_application_statuses( context: &Data, admin: LocalUserView, @@ -122,14 +116,14 @@ async fn get_application_statuses( get_unread_registration_application_count(context.reset_request_count(), admin.clone()).await?; let unread_applications = list_registration_applications( - Query::from_query("unread_only=true").unwrap(), + Query::from_query("unread_only=true")?, context.reset_request_count(), admin.clone(), ) .await?; let all_applications = list_registration_applications( - Query::from_query("unread_only=false").unwrap(), + Query::from_query("unread_only=false")?, context.reset_request_count(), admin, ) @@ -138,10 +132,9 @@ async fn get_application_statuses( Ok((application_count, unread_applications, all_applications)) } -#[allow(clippy::indexing_slicing)] -#[allow(clippy::unwrap_used)] -#[tokio::test] #[serial] +#[tokio::test] +#[expect(clippy::indexing_slicing)] async fn test_application_approval() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let pool = &mut context.pool(); diff --git a/crates/api/src/sitemap.rs b/crates/api/src/sitemap.rs index bd0e0dad8..c3c3c417c 100644 --- a/crates/api/src/sitemap.rs +++ b/crates/api/src/sitemap.rs @@ -42,44 +42,40 @@ pub async fn get_sitemap(context: Data) -> LemmyResult LemmyResult<()> { let posts: Vec<(DbUrl, DateTime)> = vec![ ( - Url::parse("https://example.com").unwrap().into(), + Url::parse("https://example.com")?.into(), NaiveDate::from_ymd_opt(2022, 12, 1) - .unwrap() + .unwrap_or_default() .and_hms_opt(9, 10, 11) - .unwrap() + .unwrap_or_default() .and_utc(), ), ( - Url::parse("https://lemmy.ml").unwrap().into(), + Url::parse("https://lemmy.ml")?.into(), NaiveDate::from_ymd_opt(2023, 1, 1) - .unwrap() + .unwrap_or_default() .and_hms_opt(1, 2, 3) - .unwrap() + .unwrap_or_default() .and_utc(), ), ]; let mut buf = Vec::::new(); - generate_urlset(posts) - .await - .unwrap() - .write(&mut buf) - .unwrap(); - let root = Element::from_reader(buf.as_slice()).unwrap(); + generate_urlset(posts).await?.write(&mut buf)?; + let root = Element::from_reader(buf.as_slice())?; assert_eq!(root.tag().name(), "urlset"); assert_eq!(root.child_count(), 2); @@ -99,45 +95,43 @@ pub(crate) mod tests { root .children() .next() - .unwrap() - .children() - .find(|element| element.tag().name() == "loc") - .unwrap() - .text(), + .and_then(|n| n.children().find(|element| element.tag().name() == "loc")) + .map(Element::text) + .unwrap_or_default(), "https://example.com/" ); assert_eq!( root .children() .next() - .unwrap() - .children() - .find(|element| element.tag().name() == "lastmod") - .unwrap() - .text(), + .and_then(|n| n + .children() + .find(|element| element.tag().name() == "lastmod")) + .map(Element::text) + .unwrap_or_default(), "2022-12-01T09:10:11+00:00" ); assert_eq!( root .children() .nth(1) - .unwrap() - .children() - .find(|element| element.tag().name() == "loc") - .unwrap() - .text(), + .and_then(|n| n.children().find(|element| element.tag().name() == "loc")) + .map(Element::text) + .unwrap_or_default(), "https://lemmy.ml/" ); assert_eq!( root .children() .nth(1) - .unwrap() - .children() - .find(|element| element.tag().name() == "lastmod") - .unwrap() - .text(), + .and_then(|n| n + .children() + .find(|element| element.tag().name() == "lastmod")) + .map(Element::text) + .unwrap_or_default(), "2023-01-01T01:02:03+00:00" ); + + Ok(()) } } diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 06e62bae3..6476f855a 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -69,8 +69,6 @@ impl Claims { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{claims::Claims, context::LemmyContext}; @@ -85,7 +83,7 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; - use lemmy_utils::rate_limit::RateLimitCell; + use lemmy_utils::{error::LemmyResult, rate_limit::RateLimitCell}; use pretty_assertions::assert_eq; use reqwest::Client; use reqwest_middleware::ClientBuilder; @@ -93,10 +91,10 @@ mod tests { #[tokio::test] #[serial] - async fn test_should_not_validate_user_token_after_password_change() { + async fn test_should_not_validate_user_token_after_password_change() -> LemmyResult<()> { let pool_ = build_db_pool_for_tests().await; let pool = &mut (&pool_).into(); - let secret = Secret::init(pool).await.unwrap().unwrap(); + let secret = Secret::init(pool).await?; let context = LemmyContext::create( pool_.clone(), ClientBuilder::new(Client::default()).build(), @@ -104,29 +102,25 @@ mod tests { RateLimitCell::with_test_config(), ); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); - let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) - .await - .unwrap(); + let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; let req = TestRequest::default().to_http_request(); - let jwt = Claims::generate(inserted_local_user.id, req, &context) - .await - .unwrap(); + let jwt = Claims::generate(inserted_local_user.id, req, &context).await?; let valid = Claims::validate(&jwt, &context).await; assert!(valid.is_ok()); - let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, num_deleted); + + Ok(()) } } diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index e1c1c5d76..f8e741a58 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -3,9 +3,13 @@ use lemmy_db_schema::{ source::site::Site, CommunityVisibility, ListingType, - PostSortType, }; -use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView, PersonView}; +use lemmy_db_views_actor::structs::{ + CommunityModeratorView, + CommunitySortType, + CommunityView, + PersonView, +}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -74,7 +78,7 @@ pub struct CommunityResponse { /// Fetches a list of communities. pub struct ListCommunities { pub type_: Option, - pub sort: Option, + pub sort: Option, pub show_nsfw: Option, pub page: Option, pub limit: Option, @@ -225,3 +229,12 @@ pub struct TransferCommunity { pub community_id: CommunityId, pub person_id: PersonId, } + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Fetches a random community +pub struct GetRandomCommunity { + pub type_: Option, +} diff --git a/crates/api_common/src/oauth_provider.rs b/crates/api_common/src/oauth_provider.rs index c51edc7a4..14847edf1 100644 --- a/crates/api_common/src/oauth_provider.rs +++ b/crates/api_common/src/oauth_provider.rs @@ -5,6 +5,7 @@ use serde_with::skip_serializing_none; use ts_rs::TS; use url::Url; +#[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] @@ -19,11 +20,12 @@ pub struct CreateOAuthProvider { pub client_id: String, pub client_secret: String, pub scopes: String, - pub auto_verify_email: bool, - pub account_linking_enabled: bool, - pub enabled: bool, + pub auto_verify_email: Option, + pub account_linking_enabled: Option, + pub enabled: Option, } +#[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 40e8df4ce..6f1ddfe43 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -84,8 +84,8 @@ pub struct CaptchaResponse { pub struct SaveUserSettings { /// Show nsfw posts. pub show_nsfw: Option, + /// Blur nsfw posts. pub blur_nsfw: Option, - pub auto_expand: Option, /// Your user's theme. pub theme: Option, /// The default post listing type, usually "local" diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index 970aa17df..b0da6cf4d 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -354,9 +354,10 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L // fetch remote non-pictrs images for persistent thumbnail link // TODO: should limit size once supported by pictrs let fetch_url = format!( - "{}image/download?url={}", + "{}image/download?url={}&resize={}", pictrs_config.url, - encode(image_url.as_str()) + encode(image_url.as_str()), + context.settings().pictrs_config()?.max_thumbnail_size ); let res = context @@ -471,14 +472,13 @@ pub async fn replace_image( } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ context::LemmyContext, request::{extract_opengraph_data, fetch_link_metadata}, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; use url::Url; @@ -486,10 +486,10 @@ mod tests { // These helped with testing #[tokio::test] #[serial] - async fn test_link_metadata() { + async fn test_link_metadata() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; - let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap(); - let sample_res = fetch_link_metadata(&sample_url, &context).await.unwrap(); + let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ")?; + let sample_res = fetch_link_metadata(&sample_url, &context).await?; assert_eq!( Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()), sample_res.opengraph_data.title @@ -500,8 +500,7 @@ mod tests { ); assert_eq!( Some( - Url::parse("https://gitlab.com/uploads/-/system/project/avatar/4877469/iod_logo.png") - .unwrap() + Url::parse("https://gitlab.com/uploads/-/system/project/avatar/4877469/iod_logo.png")? .into() ), sample_res.opengraph_data.image @@ -511,19 +510,21 @@ mod tests { Some(mime::TEXT_HTML_UTF_8.to_string()), sample_res.content_type ); + + Ok(()) } #[test] - fn test_resolve_image_url() { + fn test_resolve_image_url() -> LemmyResult<()> { // url that lists the opengraph fields - let url = Url::parse("https://example.com/one/two.html").unwrap(); + let url = Url::parse("https://example.com/one/two.html")?; // root relative url let html_bytes = b""; let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); assert_eq!( metadata.image, - Some(Url::parse("https://example.com/image.jpg").unwrap().into()) + Some(Url::parse("https://example.com/image.jpg")?.into()) ); // base relative url @@ -531,11 +532,7 @@ mod tests { let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); assert_eq!( metadata.image, - Some( - Url::parse("https://example.com/one/image.jpg") - .unwrap() - .into() - ) + Some(Url::parse("https://example.com/one/image.jpg")?.into()) ); // absolute url @@ -543,7 +540,7 @@ mod tests { let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); assert_eq!( metadata.image, - Some(Url::parse("https://cdn.host.com/image.jpg").unwrap().into()) + Some(Url::parse("https://cdn.host.com/image.jpg")?.into()) ); // protocol relative url @@ -551,7 +548,9 @@ mod tests { let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); assert_eq!( metadata.image, - Some(Url::parse("https://example.com/image.jpg").unwrap().into()) + Some(Url::parse("https://example.com/image.jpg")?.into()) ); + + Ok(()) } } diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index fc7ececc9..8316b30ee 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -21,6 +21,7 @@ use lemmy_db_schema::{ tagline::Tagline, }, CommentSortType, + FederationMode, ListingType, ModlogActionType, PostListingMode, @@ -78,7 +79,7 @@ pub struct Search { pub listing_type: Option, pub page: Option, pub limit: Option, - pub post_title_only: Option, + pub title_only: Option, pub post_url_only: Option, pub saved_only: Option, pub liked_only: Option, @@ -170,7 +171,6 @@ pub struct CreateSite { pub description: Option, pub icon: Option, pub banner: Option, - pub enable_downvotes: Option, pub enable_nsfw: Option, pub community_creation_admin_only: Option, pub require_email_verification: Option, @@ -208,6 +208,10 @@ pub struct CreateSite { pub registration_mode: Option, pub oauth_registration: Option, pub content_warning: Option, + pub post_upvotes: Option, + pub post_downvotes: Option, + pub comment_upvotes: Option, + pub comment_downvotes: Option, } #[skip_serializing_none] @@ -224,8 +228,6 @@ pub struct EditSite { pub icon: Option, /// A url for your site's banner. pub banner: Option, - /// Whether to enable downvotes. - pub enable_downvotes: Option, /// Whether to enable NSFW. pub enable_nsfw: Option, /// Limits community creation to admins only. @@ -291,13 +293,21 @@ pub struct EditSite { /// A list of blocked URLs pub blocked_urls: Option>, pub registration_mode: Option, - /// Whether or not external auth methods can auto-register users. - pub oauth_registration: Option, /// Whether to email admins for new reports. pub reports_email_admins: Option, /// If present, nsfw content is visible by default. Should be displayed by frontends/clients /// when the site is first opened by a user. pub content_warning: Option, + /// Whether or not external auth methods can auto-register users. + pub oauth_registration: Option, + /// What kind of post upvotes your site allows. + pub post_upvotes: Option, + /// What kind of post downvotes your site allows. + pub post_downvotes: Option, + /// What kind of comment upvotes your site allows. + pub comment_upvotes: Option, + /// What kind of comment downvotes your site allows. + pub comment_downvotes: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -306,6 +316,8 @@ pub struct EditSite { /// The response for a site. pub struct SiteResponse { pub site_view: SiteView, + /// deprecated, use field `tagline` or /api/v3/tagline/list + pub taglines: Vec<()>, } #[skip_serializing_none] @@ -320,6 +332,10 @@ pub struct GetSiteResponse { pub my_user: Option, pub all_languages: Vec, pub discussion_languages: Vec, + /// deprecated, use field `tagline` or /api/v3/tagline/list + pub taglines: Vec<()>, + /// deprecated, use /api/v3/custom_emoji/list + pub custom_emojis: Vec<()>, /// If the site has any taglines, a random one is included here for displaying pub tagline: Option, /// A list of external auth methods your site supports. diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 06c37ad16..e255e36c2 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -11,9 +11,9 @@ use chrono::{DateTime, Days, Local, TimeZone, Utc}; use enum_map::{enum_map, EnumMap}; use lemmy_db_schema::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, - newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId}, + newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId}, source::{ - comment::{Comment, CommentUpdateForm}, + comment::{Comment, CommentLike, CommentUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm}, community_block::CommunityBlock, email_verification::{EmailVerification, EmailVerificationForm}, @@ -23,16 +23,18 @@ use lemmy_db_schema::{ local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_site_url_blocklist::LocalSiteUrlBlocklist, + moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm}, oauth_account::OAuthAccount, password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, person_block::PersonBlock, - post::{Post, PostRead}, + post::{Post, PostLike, PostRead}, registration_application::RegistrationApplication, site::Site, }, - traits::Crud, + traits::{Crud, Likeable}, utils::DbPool, + FederationMode, RegistrationMode, }; use lemmy_db_views::{ @@ -50,7 +52,7 @@ use lemmy_utils::{ rate_limit::{ActionType, BucketConfig}, settings::structs::{PictrsImageMode, Settings}, utils::{ - markdown::{markdown_check_for_blocked_urls, markdown_rewrite_image_links}, + markdown::{image_links::markdown_rewrite_image_links, markdown_check_for_blocked_urls}, slurs::{build_slur_regex, remove_slurs}, validation::clean_urls_in_text, }, @@ -296,13 +298,36 @@ pub async fn check_person_instance_community_block( Ok(()) } +/// A vote item type used to check the vote mode. +pub enum VoteItem { + Post(PostId), + Comment(CommentId), +} + #[tracing::instrument(skip_all)] -pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> LemmyResult<()> { - if score == -1 && !local_site.enable_downvotes { - Err(LemmyErrorType::DownvotesAreDisabled)? - } else { - Ok(()) +pub async fn check_local_vote_mode( + score: i16, + vote_item: VoteItem, + local_site: &LocalSite, + person_id: PersonId, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + let (downvote_setting, upvote_setting) = match vote_item { + VoteItem::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), + VoteItem::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), + }; + + let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable; + let upvote_fail = score == 1 && upvote_setting == FederationMode::Disable; + + // Undo previous vote for item if new vote fails + if downvote_fail || upvote_fail { + match vote_item { + VoteItem::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?, + VoteItem::Comment(comment_id) => CommentLike::remove(pool, person_id, comment_id).await?, + }; } + Ok(()) } /// Dont allow bots to do certain actions, like voting @@ -667,112 +692,179 @@ pub async fn purge_image_posts_for_community( Ok(()) } -pub async fn remove_user_data( +/// Removes or restores user data. +pub async fn remove_or_restore_user_data( + mod_person_id: PersonId, banned_person_id: PersonId, + removed: bool, + reason: &Option, context: &LemmyContext, ) -> LemmyResult<()> { let pool = &mut context.pool(); - // Purge user images - let person = Person::read(pool, banned_person_id).await?; - if let Some(avatar) = person.avatar { - purge_image_from_pictrs(&avatar, context).await.ok(); - } - if let Some(banner) = person.banner { - purge_image_from_pictrs(&banner, context).await.ok(); + + // Only these actions are possible when removing, not restoring + if removed { + // Purge user images + let person = Person::read(pool, banned_person_id).await?; + if let Some(avatar) = person.avatar { + purge_image_from_pictrs(&avatar, context).await.ok(); + } + if let Some(banner) = person.banner { + purge_image_from_pictrs(&banner, context).await.ok(); + } + + // Update the fields to None + Person::update( + pool, + banned_person_id, + &PersonUpdateForm { + avatar: Some(None), + banner: Some(None), + bio: Some(None), + ..Default::default() + }, + ) + .await?; + + // Purge image posts + purge_image_posts_for_person(banned_person_id, context).await?; + + // Communities + // Remove all communities where they're the top mod + // for now, remove the communities manually + let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?; + + // Filter to only this banned users top communities + let banned_user_first_communities: Vec = first_mod_communities + .into_iter() + .filter(|fmc| fmc.moderator.id == banned_person_id) + .collect(); + + for first_mod_community in banned_user_first_communities { + let community_id = first_mod_community.community.id; + Community::update( + pool, + community_id, + &CommunityUpdateForm { + removed: Some(removed), + ..Default::default() + }, + ) + .await?; + + // Delete the community images + if let Some(icon) = first_mod_community.community.icon { + purge_image_from_pictrs(&icon, context).await.ok(); + } + if let Some(banner) = first_mod_community.community.banner { + purge_image_from_pictrs(&banner, context).await.ok(); + } + // Update the fields to None + Community::update( + pool, + community_id, + &CommunityUpdateForm { + icon: Some(None), + banner: Some(None), + ..Default::default() + }, + ) + .await?; + } } - // Update the fields to None - Person::update( + // Posts + let removed_or_restored_posts = + Post::update_removed_for_creator(pool, banned_person_id, None, removed).await?; + create_modlog_entries_for_removed_or_restored_posts( pool, - banned_person_id, - &PersonUpdateForm { - avatar: Some(None), - banner: Some(None), - bio: Some(None), - ..Default::default() - }, + mod_person_id, + removed_or_restored_posts.iter().map(|r| r.id).collect(), + removed, + reason, ) .await?; - // Posts - Post::update_removed_for_creator(pool, banned_person_id, None, true).await?; - - // Purge image posts - purge_image_posts_for_person(banned_person_id, context).await?; - - // Communities - // Remove all communities where they're the top mod - // for now, remove the communities manually - let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?; - - // Filter to only this banned users top communities - let banned_user_first_communities: Vec = first_mod_communities - .into_iter() - .filter(|fmc| fmc.moderator.id == banned_person_id) - .collect(); - - for first_mod_community in banned_user_first_communities { - let community_id = first_mod_community.community.id; - Community::update( - pool, - community_id, - &CommunityUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - - // Delete the community images - if let Some(icon) = first_mod_community.community.icon { - purge_image_from_pictrs(&icon, context).await.ok(); - } - if let Some(banner) = first_mod_community.community.banner { - purge_image_from_pictrs(&banner, context).await.ok(); - } - // Update the fields to None - Community::update( - pool, - community_id, - &CommunityUpdateForm { - icon: Some(None), - banner: Some(None), - ..Default::default() - }, - ) - .await?; - } - // Comments - Comment::update_removed_for_creator(pool, banned_person_id, true).await?; + let removed_or_restored_comments = + Comment::update_removed_for_creator(pool, banned_person_id, removed).await?; + create_modlog_entries_for_removed_or_restored_comments( + pool, + mod_person_id, + removed_or_restored_comments.iter().map(|r| r.id).collect(), + removed, + reason, + ) + .await?; Ok(()) } -/// We can't restore their images, but we can unremove their posts and comments -pub async fn restore_user_data( - banned_person_id: PersonId, - context: &LemmyContext, +async fn create_modlog_entries_for_removed_or_restored_posts( + pool: &mut DbPool<'_>, + mod_person_id: PersonId, + post_ids: Vec, + removed: bool, + reason: &Option, ) -> LemmyResult<()> { - let pool = &mut context.pool(); + // Build the forms + let forms = post_ids + .iter() + .map(|&post_id| ModRemovePostForm { + mod_person_id, + post_id, + removed: Some(removed), + reason: reason.clone(), + }) + .collect(); - // Posts - Post::update_removed_for_creator(pool, banned_person_id, None, false).await?; + ModRemovePost::create_multiple(pool, &forms).await?; - // Comments - Comment::update_removed_for_creator(pool, banned_person_id, false).await?; + Ok(()) +} + +async fn create_modlog_entries_for_removed_or_restored_comments( + pool: &mut DbPool<'_>, + mod_person_id: PersonId, + comment_ids: Vec, + removed: bool, + reason: &Option, +) -> LemmyResult<()> { + // Build the forms + let forms = comment_ids + .iter() + .map(|&comment_id| ModRemoveCommentForm { + mod_person_id, + comment_id, + removed: Some(removed), + reason: reason.clone(), + }) + .collect(); + + ModRemoveComment::create_multiple(pool, &forms).await?; Ok(()) } pub async fn remove_or_restore_user_data_in_community( community_id: CommunityId, + mod_person_id: PersonId, banned_person_id: PersonId, remove: bool, + reason: &Option, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { // Posts - Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), remove).await?; + let posts = + Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), remove).await?; + create_modlog_entries_for_removed_or_restored_posts( + pool, + mod_person_id, + posts.iter().map(|r| r.id).collect(), + remove, + reason, + ) + .await?; // Comments // TODO Diesel doesn't allow updates with joins, so this has to be a loop @@ -798,6 +890,15 @@ pub async fn remove_or_restore_user_data_in_community( .await?; } + create_modlog_entries_for_removed_or_restored_comments( + pool, + mod_person_id, + comments.iter().map(|r| r.comment.id).collect(), + remove, + reason, + ) + .await?; + Ok(()) } @@ -1067,11 +1168,20 @@ fn build_proxied_image_url( } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; + use lemmy_db_schema::source::{ + comment::CommentInsertForm, + community::CommunityInsertForm, + person::PersonInsertForm, + post::PostInsertForm, + }; + use lemmy_db_views_moderator::structs::{ + ModRemoveCommentView, + ModRemovePostView, + ModlogListParams, + }; use pretty_assertions::assert_eq; use serial_test::serial; @@ -1093,48 +1203,42 @@ mod tests { } #[test] - fn test_limit_ban_term() { + fn test_limit_ban_term() -> LemmyResult<()> { // Ban expires in past, should throw error assert!(limit_expire_time(Utc::now() - Days::new(5)).is_err()); // Legitimate ban term, return same value let fourteen_days = Utc::now() + Days::new(14); - assert_eq!( - limit_expire_time(fourteen_days).unwrap(), - Some(fourteen_days) - ); + assert_eq!(limit_expire_time(fourteen_days)?, Some(fourteen_days)); let nine_years = Utc::now() + Days::new(365 * 9); - assert_eq!(limit_expire_time(nine_years).unwrap(), Some(nine_years)); + assert_eq!(limit_expire_time(nine_years)?, Some(nine_years)); // Too long ban term, changes to None (permanent ban) - assert_eq!( - limit_expire_time(Utc::now() + Days::new(365 * 11)).unwrap(), - None - ); + assert_eq!(limit_expire_time(Utc::now() + Days::new(365 * 11))?, None); + + Ok(()) } #[tokio::test] #[serial] - async fn test_proxy_image_link() { + async fn test_proxy_image_link() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; // image from local domain is unchanged - let local_url = Url::parse("http://lemmy-alpha/image.png").unwrap(); + let local_url = Url::parse("http://lemmy-alpha/image.png")?; let proxied = proxy_image_link_internal(local_url.clone(), PictrsImageMode::ProxyAllImages, &context) - .await - .unwrap(); + .await?; assert_eq!(&local_url, proxied.inner()); // image from remote domain is proxied - let remote_image = Url::parse("http://lemmy-beta/image.png").unwrap(); + let remote_image = Url::parse("http://lemmy-beta/image.png")?; let proxied = proxy_image_link_internal( remote_image.clone(), PictrsImageMode::ProxyAllImages, &context, ) - .await - .unwrap(); + .await?; assert_eq!( "https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png", proxied.as_str() @@ -1147,5 +1251,159 @@ mod tests { .await .is_ok() ); + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_mod_remove_or_restore_data() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_mod = PersonInsertForm::test_form(inserted_instance.id, "modder"); + let inserted_mod = Person::create(pool, &new_mod).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "chrimbus"); + let inserted_person = Person::create(pool, &new_person).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "mod_community crepes".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; + + let post_form_1 = PostInsertForm::new( + "A test post tubular".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post_1 = Post::create(pool, &post_form_1).await?; + + let post_form_2 = PostInsertForm::new( + "A test post radical".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post_2 = Post::create(pool, &post_form_2).await?; + + let comment_form_1 = CommentInsertForm::new( + inserted_person.id, + inserted_post_1.id, + "A test comment tubular".into(), + ); + let _inserted_comment_1 = Comment::create(pool, &comment_form_1, None).await?; + + let comment_form_2 = CommentInsertForm::new( + inserted_person.id, + inserted_post_2.id, + "A test comment radical".into(), + ); + let _inserted_comment_2 = Comment::create(pool, &comment_form_2, None).await?; + + // Remove the user data + remove_or_restore_user_data( + inserted_mod.id, + inserted_person.id, + true, + &Some("a remove reason".to_string()), + &context, + ) + .await?; + + // Verify that their posts and comments are removed. + let params = ModlogListParams { + community_id: None, + mod_person_id: None, + other_person_id: None, + post_id: None, + comment_id: None, + page: None, + limit: None, + hide_modlog_names: false, + }; + + // Posts + let post_modlog = ModRemovePostView::list(pool, params).await?; + assert_eq!(2, post_modlog.len()); + + let mod_removed_posts = post_modlog + .iter() + .map(|p| p.mod_remove_post.removed) + .collect::>(); + assert_eq!(vec![true, true], mod_removed_posts); + + let removed_posts = post_modlog + .iter() + .map(|p| p.post.removed) + .collect::>(); + assert_eq!(vec![true, true], removed_posts); + + // Comments + let comment_modlog = ModRemoveCommentView::list(pool, params).await?; + assert_eq!(2, comment_modlog.len()); + + let mod_removed_comments = comment_modlog + .iter() + .map(|p| p.mod_remove_comment.removed) + .collect::>(); + assert_eq!(vec![true, true], mod_removed_comments); + + let removed_comments = comment_modlog + .iter() + .map(|p| p.comment.removed) + .collect::>(); + assert_eq!(vec![true, true], removed_comments); + + // Now restore the content, and make sure it got appended + remove_or_restore_user_data( + inserted_mod.id, + inserted_person.id, + false, + &Some("a restore reason".to_string()), + &context, + ) + .await?; + + // Posts + let post_modlog = ModRemovePostView::list(pool, params).await?; + assert_eq!(4, post_modlog.len()); + + let mod_restored_posts = post_modlog + .iter() + .map(|p| p.mod_remove_post.removed) + .collect::>(); + assert_eq!(vec![false, false, true, true], mod_restored_posts); + + let restored_posts = post_modlog + .iter() + .map(|p| p.post.removed) + .collect::>(); + // All of these will be false, cause its the current state of the post + assert_eq!(vec![false, false, false, false], restored_posts); + + // Comments + let comment_modlog = ModRemoveCommentView::list(pool, params).await?; + assert_eq!(4, comment_modlog.len()); + + let mod_restored_comments = comment_modlog + .iter() + .map(|p| p.mod_remove_comment.removed) + .collect::>(); + assert_eq!(vec![false, false, true, true], mod_restored_comments); + + let restored_comments = comment_modlog + .iter() + .map(|p| p.comment.removed) + .collect::>(); + assert_eq!(vec![false, false, false, false], restored_comments); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 795c27b35..f85a4d929 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -30,10 +30,9 @@ use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, + MAX_COMMENT_DEPTH_LIMIT, }; -const MAX_COMMENT_DEPTH_LIMIT: usize = 100; - #[tracing::instrument(skip(context))] pub async fn create_comment( data: Json, @@ -89,16 +88,9 @@ pub async fn create_comment( check_comment_depth(parent)?; } - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - data.language_id, - community_id, - ) - .await?; - // attempt to set default language if none was provided let language_id = match data.language_id { - Some(lid) => Some(lid), + Some(lid) => lid, None => { default_post_language( &mut context.pool(), @@ -109,8 +101,11 @@ pub async fn create_comment( } }; + CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id) + .await?; + let comment_form = CommentInsertForm { - language_id, + language_id: Some(language_id), ..CommentInsertForm::new(local_user_view.person.id, data.post_id, content.clone()) }; diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 76bdcfbb4..51f65aa67 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -55,13 +55,14 @@ pub async fn update_comment( Err(LemmyErrorType::NoCommentEditAllowed)? } - let language_id = data.language_id; - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - language_id, - orig_comment.community.id, - ) - .await?; + if let Some(language_id) = data.language_id { + CommunityLanguage::is_allowed_community_language( + &mut context.pool(), + language_id, + orig_comment.community.id, + ) + .await?; + } let slur_regex = local_site_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(&context).await?; diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index a1357395b..90c68bdbd 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -104,18 +104,9 @@ pub async fn create_post( .await?; } - // Only need to check if language is allowed in case user set it explicitly. When using default - // language, it already only returns allowed languages. - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - data.language_id, - community_id, - ) - .await?; - // attempt to set default language if none was provided let language_id = match data.language_id { - Some(lid) => Some(lid), + Some(lid) => lid, None => { default_post_language( &mut context.pool(), @@ -126,6 +117,11 @@ pub async fn create_post( } }; + // Only need to check if language is allowed in case user set it explicitly. When using default + // language, it already only returns allowed languages. + CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id) + .await?; + let scheduled_publish_time = convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?; let post_form = PostInsertForm { @@ -133,7 +129,7 @@ pub async fn create_post( body, alt_text: data.alt_text.clone(), nsfw: data.nsfw, - language_id, + language_id: Some(language_id), scheduled_publish_time, ..PostInsertForm::new( data.name.trim().to_string(), diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index 72f8309d1..cef8bfea8 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -101,13 +101,14 @@ pub async fn update_post( Err(LemmyErrorType::NoPostEditAllowed)? } - let language_id = data.language_id; - CommunityLanguage::is_allowed_community_language( - &mut context.pool(), - language_id, - orig_post.community_id, - ) - .await?; + if let Some(language_id) = data.language_id { + CommunityLanguage::is_allowed_community_language( + &mut context.pool(), + language_id, + orig_post.community_id, + ) + .await?; + } // handle changes to scheduled_publish_time let scheduled_publish_time = match ( diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index fa630b2f1..07efa72b9 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -90,7 +90,6 @@ pub async fn create_site( let local_site_form = LocalSiteUpdateForm { // Set the site setup to true site_setup: Some(true), - enable_downvotes: data.enable_downvotes, registration_mode: data.registration_mode, community_creation_admin_only: data.community_creation_admin_only, require_email_verification: data.require_email_verification, @@ -110,6 +109,10 @@ pub async fn create_site( captcha_enabled: data.captcha_enabled, captcha_difficulty: data.captcha_difficulty.clone(), default_post_listing_mode: data.default_post_listing_mode, + post_upvotes: data.post_upvotes, + post_downvotes: data.post_downvotes, + comment_upvotes: data.comment_upvotes, + comment_downvotes: data.comment_downvotes, ..Default::default() }; @@ -139,7 +142,10 @@ pub async fn create_site( local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); context.rate_limit_cell().set_config(rate_limit_config); - Ok(Json(SiteResponse { site_view })) + Ok(Json(SiteResponse { + site_view, + taglines: vec![], + })) } fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> { @@ -189,8 +195,6 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::site::create::validate_create_payload; diff --git a/crates/api_crud/src/site/mod.rs b/crates/api_crud/src/site/mod.rs index 462d2f072..48b819c38 100644 --- a/crates/api_crud/src/site/mod.rs +++ b/crates/api_crud/src/site/mod.rs @@ -48,8 +48,6 @@ fn not_zero(val: Option) -> Option { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::site::{application_question_check, not_zero, site_default_post_listing_type_check}; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 0901b9186..47fd1f154 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -43,7 +43,7 @@ pub async fn get_site( let all_languages = Language::read_all(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; - let tagline = Tagline::get_random(&mut context.pool()).await?; + let tagline = Tagline::get_random(&mut context.pool()).await.ok(); let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?; let oauth_providers = OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone()); @@ -59,6 +59,8 @@ pub async fn get_site( tagline, oauth_providers: Some(oauth_providers), admin_oauth_providers: Some(admin_oauth_providers), + taglines: vec![], + custom_emojis: vec![], }) }) .await diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index daa0bc49e..cce428cc1 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -99,7 +99,6 @@ pub async fn update_site( .ok(); let local_site_form = LocalSiteUpdateForm { - enable_downvotes: data.enable_downvotes, registration_mode: data.registration_mode, community_creation_admin_only: data.community_creation_admin_only, require_email_verification: data.require_email_verification, @@ -121,6 +120,10 @@ pub async fn update_site( reports_email_admins: data.reports_email_admins, default_post_listing_mode: data.default_post_listing_mode, oauth_registration: data.oauth_registration, + post_upvotes: data.post_upvotes, + post_downvotes: data.post_downvotes, + comment_upvotes: data.comment_upvotes, + comment_downvotes: data.comment_downvotes, ..Default::default() }; @@ -193,7 +196,10 @@ pub async fn update_site( local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); context.rate_limit_cell().set_config(rate_limit_config); - Ok(Json(SiteResponse { site_view })) + Ok(Json(SiteResponse { + site_view, + taglines: vec![], + })) } fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> LemmyResult<()> { @@ -241,8 +247,6 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::site::update::validate_update_payload; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index ec859cd49..bf17d6f8e 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -107,9 +107,7 @@ pub async fn register( check_slurs(&data.username, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?; - if Person::is_username_taken(&mut context.pool(), &data.username).await? { - return Err(LemmyErrorType::UsernameAlreadyExists)?; - } + Person::check_username_taken(&mut context.pool(), &data.username).await?; if let Some(email) = &data.email { LocalUser::check_is_email_taken(&mut context.pool(), email).await?; @@ -329,9 +327,7 @@ pub async fn authenticate_with_oauth( check_slurs(username, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?; - if Person::is_username_taken(&mut context.pool(), username).await? { - return Err(LemmyErrorType::UsernameAlreadyExists)?; - } + Person::check_username_taken(&mut context.pool(), username).await?; // We have to create a person, a local_user, and an oauth_account person = create_person( diff --git a/crates/apub/assets/lemmy/objects/group.json b/crates/apub/assets/lemmy/objects/group.json index 1b848a866..bd6e44065 100644 --- a/crates/apub/assets/lemmy/objects/group.json +++ b/crates/apub/assets/lemmy/objects/group.json @@ -3,9 +3,9 @@ "type": "Group", "preferredUsername": "tenforward", "name": "Ten Forward", - "summary": "

Lounge and recreation facility

\n
\n

Welcome to the Enterprise!.

\n", + "summary": "

Lounge and recreation facility

\n
\n

Welcome to the Enterprise!.

\n", "source": { - "content": "Lounge and recreation facility\n\n---\n\nWelcome to the [Enterprise](https://memory-alpha.fandom.com/wiki/USS_Enterprise_(NCC-1701-D))!.", + "content": "Lounge and recreation facility\n\n---\n\nWelcome to the Enterprise!", "mediaType": "text/markdown" }, "sensitive": false, diff --git a/crates/apub/assets/pleroma/objects/note.json b/crates/apub/assets/pleroma/objects/note.json index ff4b20d25..af61ff46e 100644 --- a/crates/apub/assets/pleroma/objects/note.json +++ b/crates/apub/assets/pleroma/objects/note.json @@ -10,7 +10,7 @@ "attachment": [], "attributedTo": "https://queer.hacktivis.me/users/lanodan", "cc": ["https://www.w3.org/ns/activitystreams#Public"], - "content": "@popolon Have what?", + "content": "Have what?", "context": "https://queer.hacktivis.me/contexts/34cba3d2-2f35-4169-aeff-56af9bfeb753", "conversation": "https://queer.hacktivis.me/contexts/34cba3d2-2f35-4169-aeff-56af9bfeb753", "id": "https://queer.hacktivis.me/objects/8d4973f4-53de-49cd-8c27-df160e16a9c2", diff --git a/crates/apub/assets/pleroma/objects/person.json b/crates/apub/assets/pleroma/objects/person.json index bc9008bab..fff9a2cba 100644 --- a/crates/apub/assets/pleroma/objects/person.json +++ b/crates/apub/assets/pleroma/objects/person.json @@ -41,7 +41,7 @@ "owner": "https://queer.hacktivis.me/users/lanodan", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsWOgdjSMc010qvxC3njI\nXJlFWMJ5gJ8QXCW/PajYdsHPM6d+jxBNJ6zp9/tIRa2m7bWHTSkuHQ7QthOpt6vu\n+dAWpKRLS607SPLItn/qUcyXvgN+H8shfyhMxvkVs9jXdtlBsLUVE7UNpN0dxzqe\nI79QWbf7o4amgaIWGRYB+OYMnIxKt+GzIkivZdSVSYjfxNnBYkMCeUxm5EpPIxKS\nP5bBHAVRRambD5NUmyKILuC60/rYuc/C+vmgpY2HCWFS2q6o34dPr9enwL6t4b3m\nS1t/EJHk9rGaaDqSGkDEfyQI83/7SDebWKuETMKKFLZi1vMgQIFuOYCIhN6bIiZm\npQIDAQAB\n-----END PUBLIC KEY-----\n\n" }, - "summary": "---
Website: https://hacktivis.me/
Lang: Français(natif), English(fluent), LSF(🤏~👌), русский (еле-еле),
Politics: Anarchist as in DIY/DIWO, freedom of association, anti-authoritarian, anti-identitarianism

Pronouns: meh, pick any, have fun
Timezone: Let's say Mars, I have a non-24h cycle
```
🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:
Pleroma maintainer (mostly backend)
BadWolf developer
Gentoo contributor

Dayjob: yogoko.fr

That person which uses HJKL in games

Just because computer bad: X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

banner from: https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db
Federation-bots: #nobot", + "summary": "---Lang: Français(natif), English(fluent), LSF(🤏~👌), русский (еле-еле),
Politics: Anarchist as in DIY/DIWO, freedom of association, anti-authoritarian, anti-identitarianism

Pronouns: meh, pick any, have fun
Timezone: Let's say Mars, I have a non-24h cycle
```
🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:
Pleroma maintainer (mostly backend)
BadWolf developer
Gentoo contributor

Dayjob: yogoko.fr

That person which uses HJKL in games

Just because computer bad: X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

banner from: https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db
Federation-bots: #nobot", "tag": [ { "icon": { diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 29412cdfe..e291cc4a4 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -23,7 +23,7 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, - utils::{remove_or_restore_user_data_in_community, remove_user_data}, + utils::{remove_or_restore_user_data, remove_or_restore_user_data_in_community}, }; use lemmy_db_schema::{ source::{ @@ -160,6 +160,7 @@ impl ActivityHandler for BlockUser { let mod_person = self.actor.dereference(context).await?; let blocked_person = self.object.dereference(context).await?; let target = self.target.dereference(context).await?; + let reason = self.summary; match target { SiteOrCommunity::Site(_site) => { let blocked_person = Person::update( @@ -173,14 +174,15 @@ impl ActivityHandler for BlockUser { ) .await?; if self.remove_data.unwrap_or(false) { - remove_user_data(blocked_person.id, context).await?; + remove_or_restore_user_data(mod_person.id, blocked_person.id, true, &reason, context) + .await?; } // write mod log let form = ModBanForm { mod_person_id: mod_person.id, other_person_id: blocked_person.id, - reason: self.summary, + reason, banned: Some(true), expires, }; @@ -207,8 +209,10 @@ impl ActivityHandler for BlockUser { if self.remove_data.unwrap_or(false) { remove_or_restore_user_data_in_community( community.id, + mod_person.id, blocked_person.id, true, + &reason, &mut context.pool(), ) .await?; @@ -219,7 +223,7 @@ impl ActivityHandler for BlockUser { mod_person_id: mod_person.id, other_person_id: blocked_person.id, community_id: community.id, - reason: self.summary, + reason, banned: Some(true), expires, }; diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index 416770348..f9f6890b6 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -19,7 +19,7 @@ use activitypub_federation::{ }; use lemmy_api_common::{ context::LemmyContext, - utils::{remove_or_restore_user_data_in_community, restore_user_data}, + utils::{remove_or_restore_user_data, remove_or_restore_user_data_in_community}, }; use lemmy_db_schema::{ source::{ @@ -120,7 +120,8 @@ impl ActivityHandler for UndoBlockUser { .await?; if self.restore_data.unwrap_or(false) { - restore_user_data(blocked_person.id, context).await?; + remove_or_restore_user_data(mod_person.id, blocked_person.id, false, &None, context) + .await?; } // write mod log @@ -144,8 +145,10 @@ impl ActivityHandler for UndoBlockUser { if self.restore_data.unwrap_or(false) { remove_or_restore_user_data_in_community( community.id, + mod_person.id, blocked_person.id, false, + &None, &mut context.pool(), ) .await?; diff --git a/crates/apub/src/activities/voting/vote.rs b/crates/apub/src/activities/voting/vote.rs index 324c8b300..1cdc81952 100644 --- a/crates/apub/src/activities/voting/vote.rs +++ b/crates/apub/src/activities/voting/vote.rs @@ -18,7 +18,7 @@ use activitypub_federation::{ traits::{ActivityHandler, Actor}, }; use lemmy_api_common::{context::LemmyContext, utils::check_bot_account}; -use lemmy_db_schema::source::local_site::LocalSite; +use lemmy_db_schema::{source::local_site::LocalSite, FederationMode}; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; @@ -68,12 +68,22 @@ impl ActivityHandler for Vote { check_bot_account(&actor.0)?; - let enable_downvotes = LocalSite::read(&mut context.pool()) + // Check for enabled federation votes + let local_site = LocalSite::read(&mut context.pool()) .await - .map(|l| l.enable_downvotes) - .unwrap_or(true); - if self.kind == VoteType::Dislike && !enable_downvotes { - // If this is a downvote but downvotes are ignored, only undo any existing vote + .unwrap_or_default(); + + let (downvote_setting, upvote_setting) = match object { + PostOrComment::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), + PostOrComment::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), + }; + + // Don't allow dislikes for either disabled, or local only votes + let downvote_fail = self.kind == VoteType::Dislike && downvote_setting != FederationMode::All; + let upvote_fail = self.kind == VoteType::Like && upvote_setting != FederationMode::All; + + if downvote_fail || upvote_fail { + // If this is a rejection, undo the vote match object { PostOrComment::Post(p) => undo_vote_post(actor, &p, context).await, PostOrComment::Comment(c) => undo_vote_comment(actor, &c, context).await, diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index e895ad1cc..9262236d8 100644 --- a/crates/apub/src/activity_lists.rs +++ b/crates/apub/src/activity_lists.rs @@ -123,7 +123,6 @@ impl InCommunity for AnnouncableActivities { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index b3061d1ce..81329a97b 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -1,4 +1,5 @@ use crate::fetcher::{ + post_or_comment::PostOrComment, search::{search_query_to_object_id, search_query_to_object_id_local, SearchableObjects}, user_or_community::UserOrCommunity, }; @@ -46,21 +47,22 @@ async fn convert_response( local_user_view: Option, pool: &mut DbPool<'_>, ) -> LemmyResult> { - use SearchableObjects::*; let removed_or_deleted; let mut res = ResolveObjectResponse::default(); let local_user = local_user_view.map(|l| l.local_user); match object { - Post(p) => { - removed_or_deleted = p.deleted || p.removed; - res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), false).await?) - } - Comment(c) => { - removed_or_deleted = c.deleted || c.removed; - res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?) - } - PersonOrCommunity(p) => match *p { + SearchableObjects::PostOrComment(pc) => match *pc { + PostOrComment::Post(p) => { + removed_or_deleted = p.deleted || p.removed; + res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), false).await?) + } + PostOrComment::Comment(c) => { + removed_or_deleted = c.deleted || c.removed; + res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?) + } + }, + SearchableObjects::PersonOrCommunity(pc) => match *pc { UserOrCommunity::User(u) => { removed_or_deleted = u.deleted; res.person = Some(PersonView::read(pool, u.id).await?) diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index 642c32efa..cdc9bc55e 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -12,7 +12,11 @@ use lemmy_db_views::{ post_view::PostQuery, structs::{LocalUserView, SiteView}, }; -use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery}; +use lemmy_db_views_actor::{ + community_view::CommunityQuery, + person_view::PersonQuery, + structs::CommunitySortType, +}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -47,7 +51,7 @@ pub async fn search( listing_type, page, limit, - post_title_only, + title_only, post_url_only, saved_only, liked_only, @@ -78,7 +82,7 @@ pub async fn search( search_term: Some(q.clone()), page, limit, - title_only: post_title_only, + title_only, url_only: post_url_only, liked_only, disliked_only, @@ -102,9 +106,10 @@ pub async fn search( }; let community_query = CommunityQuery { - sort, + sort: sort.map(CommunitySortType::from), listing_type, search_term: Some(q.clone()), + title_only, local_user, is_mod_or_admin: is_admin, page, diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 6764f62cb..36a6907a0 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -103,13 +103,16 @@ pub async fn import_settings( context: Data, ) -> LemmyResult> { let person_form = PersonUpdateForm { - display_name: Some(data.display_name.clone()), - bio: Some(data.bio.clone()), - matrix_user_id: Some(data.matrix_id.clone()), + display_name: data.display_name.clone().map(Some), + bio: data.bio.clone().map(Some), + matrix_user_id: data.bio.clone().map(Some), bot_account: data.bot_account, ..Default::default() }; - Person::update(&mut context.pool(), local_user_view.person.id, &person_form).await?; + // ignore error in case form is empty + Person::update(&mut context.pool(), local_user_view.person.id, &person_form) + .await + .ok(); let local_user_form = LocalUserUpdateForm { show_nsfw: data.settings.as_ref().map(|s| s.show_nsfw), @@ -127,7 +130,6 @@ pub async fn import_settings( show_read_posts: data.settings.as_ref().map(|s| s.show_read_posts), open_links_in_new_tab: data.settings.as_ref().map(|s| s.open_links_in_new_tab), blur_nsfw: data.settings.as_ref().map(|s| s.blur_nsfw), - auto_expand: data.settings.as_ref().map(|s| s.auto_expand), infinite_scroll_enabled: data.settings.as_ref().map(|s| s.infinite_scroll_enabled), post_listing_mode: data.settings.as_ref().map(|s| s.post_listing_mode), ..Default::default() @@ -308,12 +310,14 @@ where }); Ok(failed_items.into_iter().join(",")) } -#[cfg(test)] -#[allow(clippy::indexing_slicing)] -mod tests { - use crate::api::user_settings_backup::{export_settings, import_settings, UserSettingsBackup}; +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +pub(crate) mod tests { + + use crate::api::user_settings_backup::{export_settings, import_settings}; use activitypub_federation::config::Data; + use actix_web::web::Json; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ source::{ @@ -332,7 +336,7 @@ mod tests { use std::time::Duration; use tokio::time::sleep; - async fn create_user( + pub(crate) async fn create_user( name: String, bio: Option, context: &Data, @@ -401,45 +405,6 @@ mod tests { Ok(()) } - #[tokio::test] - #[serial] - async fn test_settings_partial_import() -> LemmyResult<()> { - let context = LemmyContext::init_test_context().await; - - let export_user = - create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; - - let community_form = CommunityInsertForm::new( - export_user.person.instance_id, - "testcom".to_string(), - "testcom".to_string(), - "pubkey".to_string(), - ); - let community = Community::create(&mut context.pool(), &community_form).await?; - let follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: export_user.person.id, - pending: false, - }; - CommunityFollower::follow(&mut context.pool(), &follower_form).await?; - - let backup = export_settings(export_user.clone(), context.reset_request_count()).await?; - - let import_user = create_user("charles".to_string(), None, &context).await?; - - let backup2 = UserSettingsBackup { - followed_communities: backup.followed_communities.clone(), - ..Default::default() - }; - import_settings( - actix_web::web::Json(backup2), - import_user.clone(), - context.reset_request_count(), - ) - .await?; - Ok(()) - } - #[tokio::test] #[serial] async fn disallow_large_backup() -> LemmyResult<()> { @@ -475,4 +440,33 @@ mod tests { LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?; Ok(()) } + + #[tokio::test] + #[serial] + async fn import_partial_backup() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + + let import_user = + create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; + + let backup = + serde_json::from_str("{\"bot_account\": true, \"settings\": {\"theme\": \"my_theme\"}}")?; + import_settings( + Json(backup), + import_user.clone(), + context.reset_request_count(), + ) + .await?; + + let import_user_updated = + LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?; + // mark as bot account + assert!(import_user_updated.person.bot_account); + // dont remove existing bio + assert_eq!(import_user.person.bio, import_user_updated.person.bio); + // local_user can be deserialized without id/person_id fields + assert_eq!("my_theme", import_user_updated.local_user.theme); + + Ok(()) + } } diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs index 8e5419c7e..c7b925f97 100644 --- a/crates/apub/src/collections/community_moderators.rs +++ b/crates/apub/src/collections/community_moderators.rs @@ -98,7 +98,7 @@ impl Collection for ApubCommunityModerators { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use super::*; diff --git a/crates/apub/src/fetcher/markdown_links.rs b/crates/apub/src/fetcher/markdown_links.rs new file mode 100644 index 000000000..168958589 --- /dev/null +++ b/crates/apub/src/fetcher/markdown_links.rs @@ -0,0 +1,191 @@ +use super::{search::SearchableObjects, user_or_community::UserOrCommunity}; +use crate::fetcher::post_or_comment::PostOrComment; +use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; +use lemmy_api_common::{ + context::LemmyContext, + utils::{generate_local_apub_endpoint, EndpointType}, +}; +use lemmy_db_schema::{newtypes::InstanceId, source::instance::Instance}; +use lemmy_utils::{ + error::LemmyResult, + utils::markdown::image_links::{markdown_find_links, markdown_handle_title}, +}; +use url::Url; + +pub async fn markdown_rewrite_remote_links_opt( + src: Option, + context: &Data, +) -> Option { + match src { + Some(t) => Some(markdown_rewrite_remote_links(t, context).await), + None => None, + } +} + +/// Goes through all remote markdown links and attempts to resolve them as Activitypub objects. +/// If successful, the link is rewritten to a local link, so it can be viewed without leaving the +/// local instance. +/// +/// As it relies on ObjectId::dereference, it can only be used for incoming federated objects, not +/// for the API. +pub async fn markdown_rewrite_remote_links( + mut src: String, + context: &Data, +) -> String { + let links_offsets = markdown_find_links(&src); + + // Go through the collected links in reverse order + for (start, end) in links_offsets.into_iter().rev() { + let (url, extra) = markdown_handle_title(&src, start, end); + + if let Some(local_url) = to_local_url(url, context).await { + let mut local_url = local_url.to_string(); + // restore title + if let Some(extra) = extra { + local_url = format!("{local_url} {extra}"); + } + src.replace_range(start..end, local_url.as_str()); + } + } + + src +} + +pub(crate) async fn to_local_url(url: &str, context: &Data) -> Option { + let local_domain = &context.settings().get_protocol_and_hostname(); + let object_id = ObjectId::::parse(url).ok()?; + if object_id.inner().domain() == Some(local_domain) { + return None; + } + let dereferenced = object_id.dereference(context).await.ok()?; + match dereferenced { + SearchableObjects::PostOrComment(pc) => match *pc { + PostOrComment::Post(post) => { + generate_local_apub_endpoint(EndpointType::Post, &post.id.to_string(), local_domain) + } + PostOrComment::Comment(comment) => { + generate_local_apub_endpoint(EndpointType::Comment, &comment.id.to_string(), local_domain) + } + } + .ok() + .map(Into::into), + SearchableObjects::PersonOrCommunity(pc) => match *pc { + UserOrCommunity::User(user) => { + format_actor_url(&user.name, "u", user.instance_id, context).await + } + UserOrCommunity::Community(community) => { + format_actor_url(&community.name, "c", community.instance_id, context).await + } + } + .ok(), + } +} + +async fn format_actor_url( + name: &str, + kind: &str, + instance_id: InstanceId, + context: &LemmyContext, +) -> LemmyResult { + let local_protocol_and_hostname = context.settings().get_protocol_and_hostname(); + let local_hostname = &context.settings().hostname; + let instance = Instance::read(&mut context.pool(), instance_id).await?; + let url = if &instance.domain != local_hostname { + format!( + "{local_protocol_and_hostname}/{kind}/{name}@{}", + instance.domain + ) + } else { + format!("{local_protocol_and_hostname}/{kind}/{name}") + }; + Ok(Url::parse(&url)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::user_settings_backup::tests::create_user; + use lemmy_db_schema::{ + source::{ + community::{Community, CommunityInsertForm}, + post::{Post, PostInsertForm}, + }, + traits::Crud, + }; + use pretty_assertions::assert_eq; + use serial_test::serial; + + #[serial] + #[tokio::test] + async fn test_markdown_rewrite_remote_links() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?; + let community = Community::create( + &mut context.pool(), + &CommunityInsertForm::new( + instance.id, + "my_community".to_string(), + "My Community".to_string(), + "pubkey".to_string(), + ), + ) + .await?; + let user = create_user("john".to_string(), None, &context).await?; + + // insert a remote post which is already fetched + let post_form = PostInsertForm { + ap_id: Some(Url::parse("https://example.com/post/123")?.into()), + ..PostInsertForm::new("My post".to_string(), user.person.id, community.id) + }; + let post = Post::create(&mut context.pool(), &post_form).await?; + let markdown_local_post_url = format!("[link](https://lemmy-alpha/post/{})", post.id); + + let tests: Vec<_> = vec![ + ( + "rewrite remote post link", + format!("[link]({})", post.ap_id), + markdown_local_post_url.as_ref(), + ), + ( + "rewrite community link", + format!("[link]({})", community.actor_id), + "[link](https://lemmy-alpha/c/my_community@example.com)", + ), + ( + "dont rewrite local post link", + "[link](https://lemmy-alpha/post/2)".to_string(), + "[link](https://lemmy-alpha/post/2)", + ), + ( + "dont rewrite local community link", + "[link](https://lemmy-alpha/c/test)".to_string(), + "[link](https://lemmy-alpha/c/test)", + ), + ( + "dont rewrite non-fediverse link", + "[link](https://example.com/)".to_string(), + "[link](https://example.com/)", + ), + ( + "dont rewrite invalid url", + "[link](example-com)".to_string(), + "[link](example-com)", + ), + ]; + + let context = LemmyContext::init_test_context().await; + for (msg, input, expected) in &tests { + let result = markdown_rewrite_remote_links(input.to_string(), &context).await; + + assert_eq!( + &result, expected, + "Testing {}, with original input '{}'", + msg, input + ); + } + + Instance::delete(&mut context.pool(), instance.id).await?; + + Ok(()) + } +} diff --git a/crates/apub/src/fetcher/mod.rs b/crates/apub/src/fetcher/mod.rs index 68fc07d30..29202004f 100644 --- a/crates/apub/src/fetcher/mod.rs +++ b/crates/apub/src/fetcher/mod.rs @@ -10,6 +10,7 @@ use lemmy_db_schema::traits::ApubActor; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyError, LemmyResult}; +pub(crate) mod markdown_links; pub mod post_or_comment; pub mod search; pub mod site_or_community_or_user; diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 76c284820..e8c029106 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -1,8 +1,5 @@ -use crate::{ - fetcher::user_or_community::{PersonOrGroup, UserOrCommunity}, - objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost}, - protocol::objects::{note::Note, page::Page}, -}; +use super::post_or_comment::{PageOrNote, PostOrComment}; +use crate::fetcher::user_or_community::{PersonOrGroup, UserOrCommunity}; use activitypub_federation::{ config::Data, fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor}, @@ -54,16 +51,14 @@ pub(crate) async fn search_query_to_object_id_local( /// The types of ActivityPub objects that can be fetched directly by searching for their ID. #[derive(Debug)] pub(crate) enum SearchableObjects { - Post(ApubPost), - Comment(ApubComment), + PostOrComment(Box), PersonOrCommunity(Box), } #[derive(Deserialize)] #[serde(untagged)] pub(crate) enum SearchableKinds { - Page(Box), - Note(Note), + PageOrNote(Box), PersonOrGroup(Box), } @@ -75,8 +70,7 @@ impl Object for SearchableObjects { fn last_refreshed_at(&self) -> Option> { match self { - SearchableObjects::Post(p) => p.last_refreshed_at(), - SearchableObjects::Comment(c) => c.last_refreshed_at(), + SearchableObjects::PostOrComment(p) => p.last_refreshed_at(), SearchableObjects::PersonOrCommunity(p) => p.last_refreshed_at(), } } @@ -95,13 +89,9 @@ impl Object for SearchableObjects { if let Some(uc) = uc { return Ok(Some(SearchableObjects::PersonOrCommunity(Box::new(uc)))); } - let p = ApubPost::read_from_id(object_id.clone(), context).await?; - if let Some(p) = p { - return Ok(Some(SearchableObjects::Post(p))); - } - let c = ApubComment::read_from_id(object_id, context).await?; - if let Some(c) = c { - return Ok(Some(SearchableObjects::Comment(c))); + let pc = PostOrComment::read_from_id(object_id.clone(), context).await?; + if let Some(pc) = pc { + return Ok(Some(SearchableObjects::PostOrComment(Box::new(pc)))); } Ok(None) } @@ -109,25 +99,16 @@ impl Object for SearchableObjects { #[tracing::instrument(skip_all)] async fn delete(self, data: &Data) -> LemmyResult<()> { match self { - SearchableObjects::Post(p) => p.delete(data).await, - SearchableObjects::Comment(c) => c.delete(data).await, - SearchableObjects::PersonOrCommunity(pc) => match *pc { - UserOrCommunity::User(p) => p.delete(data).await, - UserOrCommunity::Community(c) => c.delete(data).await, - }, + SearchableObjects::PostOrComment(pc) => pc.delete(data).await, + SearchableObjects::PersonOrCommunity(pc) => pc.delete(data).await, } } async fn into_json(self, data: &Data) -> LemmyResult { + use SearchableObjects::*; Ok(match self { - SearchableObjects::Post(p) => SearchableKinds::Page(Box::new(p.into_json(data).await?)), - SearchableObjects::Comment(c) => SearchableKinds::Note(c.into_json(data).await?), - SearchableObjects::PersonOrCommunity(pc) => { - SearchableKinds::PersonOrGroup(Box::new(match *pc { - UserOrCommunity::User(p) => PersonOrGroup::Person(p.into_json(data).await?), - UserOrCommunity::Community(c) => PersonOrGroup::Group(c.into_json(data).await?), - })) - } + PostOrComment(pc) => SearchableKinds::PageOrNote(Box::new(pc.into_json(data).await?)), + PersonOrCommunity(pc) => SearchableKinds::PersonOrGroup(Box::new(pc.into_json(data).await?)), }) } @@ -137,24 +118,20 @@ impl Object for SearchableObjects { expected_domain: &Url, data: &Data, ) -> LemmyResult<()> { + use SearchableKinds::*; match apub { - SearchableKinds::Page(a) => ApubPost::verify(a, expected_domain, data).await, - SearchableKinds::Note(a) => ApubComment::verify(a, expected_domain, data).await, - SearchableKinds::PersonOrGroup(pg) => match pg.as_ref() { - PersonOrGroup::Person(a) => ApubPerson::verify(a, expected_domain, data).await, - PersonOrGroup::Group(a) => ApubCommunity::verify(a, expected_domain, data).await, - }, + PageOrNote(pn) => PostOrComment::verify(pn, expected_domain, data).await, + PersonOrGroup(pg) => UserOrCommunity::verify(pg, expected_domain, data).await, } } #[tracing::instrument(skip_all)] async fn from_json(apub: Self::Kind, context: &Data) -> LemmyResult { - use SearchableKinds as SAT; + use SearchableKinds::*; use SearchableObjects as SO; Ok(match apub { - SAT::Page(p) => SO::Post(ApubPost::from_json(*p, context).await?), - SAT::Note(n) => SO::Comment(ApubComment::from_json(n, context).await?), - SAT::PersonOrGroup(pg) => { + PageOrNote(pg) => SO::PostOrComment(Box::new(PostOrComment::from_json(*pg, context).await?)), + PersonOrGroup(pg) => { SO::PersonOrCommunity(Box::new(UserOrCommunity::from_json(*pg, context).await?)) } }) diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 424ce58b2..37482aedb 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -104,8 +104,6 @@ pub(crate) async fn get_apub_community_featured( } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] pub(crate) mod tests { use super::*; @@ -167,7 +165,7 @@ pub(crate) mod tests { } async fn decode_response(res: HttpResponse) -> LemmyResult { - let body = to_bytes(res.into_body()).await.unwrap(); + let body = to_bytes(res.into_body()).await.unwrap_or_default(); let body = std::str::from_utf8(&body)?; Ok(serde_json::from_str(body)?) } diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index ad7d84a6f..b6b411ec6 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,6 +1,7 @@ use crate::{ activities::{verify_is_public, verify_person_in_community}, check_apub_id_valid_with_strictness, + fetcher::markdown_links::markdown_rewrite_remote_links, mentions::collect_non_local_mentions, objects::{read_from_string_or_source, verify_is_remote_object}, protocol::{ @@ -104,7 +105,7 @@ impl Object for ApubComment { } else { post.ap_id.into() }; - let language = LanguageTag::new_single(self.language_id, &mut context.pool()).await?; + let language = Some(LanguageTag::new_single(self.language_id, &mut context.pool()).await?); let maa = collect_non_local_mentions(&self, community.actor_id.clone().into(), context).await?; let note = Note { @@ -128,6 +129,8 @@ impl Object for ApubComment { Ok(note) } + /// Recursively fetches all parent comments. This can lead to a stack overflow so we need to + /// Box::pin all large futures on the heap. #[tracing::instrument(skip_all)] async fn verify( note: &Note, @@ -137,14 +140,24 @@ impl Object for ApubComment { verify_domains_match(note.id.inner(), expected_domain)?; verify_domains_match(note.attributed_to.inner(), note.id.inner())?; verify_is_public(¬e.to, ¬e.cc)?; - let community = note.community(context).await?; + let community = Box::pin(note.community(context)).await?; - check_apub_id_valid_with_strictness(note.id.inner(), community.local, context).await?; + Box::pin(check_apub_id_valid_with_strictness( + note.id.inner(), + community.local, + context, + )) + .await?; verify_is_remote_object(¬e.id, context)?; - verify_person_in_community(¬e.attributed_to, &community, context).await?; + Box::pin(verify_person_in_community( + ¬e.attributed_to, + &community, + context, + )) + .await?; - let (post, _) = note.get_parents(context).await?; - let creator = note.attributed_to.dereference(context).await?; + let (post, _) = Box::pin(note.get_parents(context)).await?; + let creator = Box::pin(note.attributed_to.dereference(context)).await?; let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &creator, community.id) .await .is_ok(); @@ -169,8 +182,11 @@ impl Object for ApubComment { let slur_regex = &local_site_opt_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(context).await?; let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?; - let language_id = - LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?; + let content = markdown_rewrite_remote_links(content, context).await; + let language_id = Some( + LanguageTag::to_language_id_single(note.language.unwrap_or_default(), &mut context.pool()) + .await?, + ); let form = CommentInsertForm { creator_id: creator.id, @@ -284,7 +300,7 @@ pub(crate) mod tests { let comment = ApubComment::from_json(json, &context).await?; assert_eq!(comment.ap_id, pleroma_url.into()); - assert_eq!(comment.content.len(), 64); + assert_eq!(comment.content.len(), 10); assert!(!comment.local); assert_eq!(context.request_count(), 1); diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index d7e2490a7..2c60b87b3 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -1,6 +1,7 @@ use crate::{ activities::GetActorType, check_apub_id_valid, + fetcher::markdown_links::markdown_rewrite_remote_links_opt, local_site_data_cached, objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt}, protocol::{ @@ -148,6 +149,7 @@ impl Object for ApubCommunity { let description = read_from_string_or_source_opt(&group.summary, &None, &group.source); let description = process_markdown_opt(&description, slur_regex, &url_blocklist, context).await?; + let description = markdown_rewrite_remote_links_opt(description, context).await; let icon = proxy_image_link_opt_apub(group.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(group.image.map(|i| i.url), context).await?; @@ -296,7 +298,7 @@ pub(crate) mod tests { assert!(!community.local); assert_eq!( community.description.as_ref().map(std::string::String::len), - Some(132) + Some(63) ); Community::delete(&mut context.pool(), community.id).await?; diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index c67a223e0..6ee0a41dc 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -2,6 +2,7 @@ use super::verify_is_remote_object; use crate::{ activities::GetActorType, check_apub_id_valid_with_strictness, + fetcher::markdown_links::markdown_rewrite_remote_links_opt, local_site_data_cached, objects::read_from_string_or_source_opt, protocol::{ @@ -151,6 +152,7 @@ impl Object for ApubSite { let url_blocklist = get_url_blocklist(context).await?; let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.source); let sidebar = process_markdown_opt(&sidebar, slur_regex, &url_blocklist, context).await?; + let sidebar = markdown_rewrite_remote_links_opt(sidebar, context).await; let icon = proxy_image_link_opt_apub(apub.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(apub.image.map(|i| i.url), context).await?; diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 61ff04622..406a77d94 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -2,6 +2,7 @@ use super::verify_is_remote_object; use crate::{ activities::GetActorType, check_apub_id_valid_with_strictness, + fetcher::markdown_links::markdown_rewrite_remote_links_opt, local_site_data_cached, objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt}, protocol::{ @@ -156,6 +157,7 @@ impl Object for ApubPerson { let url_blocklist = get_url_blocklist(context).await?; let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source); let bio = process_markdown_opt(&bio, slur_regex, &url_blocklist, context).await?; + let bio = markdown_rewrite_remote_links_opt(bio, context).await; let avatar = proxy_image_link_opt_apub(person.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(person.image.map(|i| i.url), context).await?; @@ -277,7 +279,7 @@ pub(crate) mod tests { assert_eq!(person.name, "lanodan"); assert!(!person.local); assert_eq!(context.request_count(), 0); - assert_eq!(person.bio.as_ref().map(std::string::String::len), Some(873)); + assert_eq!(person.bio.as_ref().map(std::string::String::len), Some(812)); cleanup((person, site), &context).await?; Ok(()) diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index dfc9d79f9..ee88cf3ec 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,6 +1,7 @@ use crate::{ activities::{verify_is_public, verify_person_in_community}, check_apub_id_valid_with_strictness, + fetcher::markdown_links::{markdown_rewrite_remote_links_opt, to_local_url}, local_site_data_cached, objects::{read_from_string_or_source_opt, verify_is_remote_object}, protocol::{ @@ -110,7 +111,7 @@ impl Object for ApubPost { let creator = Person::read(&mut context.pool(), creator_id).await?; let community_id = self.community_id; let community = Community::read(&mut context.pool(), community_id).await?; - let language = LanguageTag::new_single(self.language_id, &mut context.pool()).await?; + let language = Some(LanguageTag::new_single(self.language_id, &mut context.pool()).await?); let attachment = self .url @@ -226,10 +227,13 @@ impl Object for ApubPost { let url_blocklist = get_url_blocklist(context).await?; - if let Some(url) = &url { - is_url_blocked(url, &url_blocklist)?; - is_valid_url(url)?; - } + let url = if let Some(url) = url { + is_url_blocked(&url, &url_blocklist)?; + is_valid_url(&url)?; + to_local_url(url.as_str(), context).await.or(Some(url)) + } else { + None + }; let alt_text = first_attachment.cloned().and_then(Attachment::alt_text); @@ -237,8 +241,11 @@ impl Object for ApubPost { let body = read_from_string_or_source_opt(&page.content, &page.media_type, &page.source); let body = process_markdown_opt(&body, slur_regex, &url_blocklist, context).await?; - let language_id = - LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?; + let body = markdown_rewrite_remote_links_opt(body, context).await; + let language_id = Some( + LanguageTag::to_language_id_single(page.language.unwrap_or_default(), &mut context.pool()) + .await?, + ); let form = PostInsertForm { url: url.map(Into::into), @@ -301,7 +308,7 @@ mod tests { assert_eq!(post.body.as_ref().map(std::string::String::len), Some(45)); assert!(!post.locked); assert!(!post.featured_community); - assert_eq!(context.request_count(), 0); + assert_eq!(context.request_count(), 1); Post::delete(&mut context.pool(), post.id).await?; Person::delete(&mut context.pool(), person.id).await?; diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index d3ca340db..3ed5b3572 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -1,6 +1,7 @@ use super::verify_is_remote_object; use crate::{ check_apub_id_valid_with_strictness, + fetcher::markdown_links::markdown_rewrite_remote_links, objects::read_from_string_or_source, protocol::{ objects::chat_message::{ChatMessage, ChatMessageType}, @@ -134,6 +135,7 @@ impl Object for ApubPrivateMessage { let url_blocklist = get_url_blocklist(context).await?; let content = read_from_string_or_source(¬e.content, &None, ¬e.source); let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?; + let content = markdown_rewrite_remote_links(content, context).await; let form = PrivateMessageInsertForm { creator_id: creator.id, diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs index a9eb74e0c..dbba1bb8a 100644 --- a/crates/apub/src/protocol/objects/mod.rs +++ b/crates/apub/src/protocol/objects/mod.rs @@ -30,21 +30,30 @@ pub(crate) struct LanguageTag { pub(crate) name: String, } +impl Default for LanguageTag { + fn default() -> Self { + LanguageTag { + identifier: "und".to_string(), + name: "Undetermined".to_string(), + } + } +} + impl LanguageTag { pub(crate) async fn new_single( lang: LanguageId, pool: &mut DbPool<'_>, - ) -> LemmyResult> { + ) -> LemmyResult { let lang = Language::read_from_id(pool, lang).await?; // undetermined if lang.id == UNDETERMINED_ID { - Ok(None) + Ok(LanguageTag::default()) } else { - Ok(Some(LanguageTag { + Ok(LanguageTag { identifier: lang.code, name: lang.name, - })) + }) } } @@ -69,13 +78,10 @@ impl LanguageTag { } pub(crate) async fn to_language_id_single( - lang: Option, + lang: Self, pool: &mut DbPool<'_>, - ) -> LemmyResult> { - let identifier = lang.map(|l| l.identifier); - let language = Language::read_id_from_code(pool, identifier.as_deref()).await?; - - Ok(language) + ) -> LemmyResult { + Ok(Language::read_id_from_code(pool, &lang.identifier).await?) } pub(crate) async fn to_language_id_multiple( @@ -86,10 +92,10 @@ impl LanguageTag { for l in langs { let id = l.identifier; - language_ids.push(Language::read_id_from_code(pool, Some(&id)).await?); + language_ids.push(Language::read_id_from_code(pool, &id).await?); } - Ok(language_ids.into_iter().flatten().collect()) + Ok(language_ids.into_iter().collect()) } } diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs index a092cec9f..e3e204254 100644 --- a/crates/apub/src/protocol/objects/note.rs +++ b/crates/apub/src/protocol/objects/note.rs @@ -20,10 +20,9 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::{error::LemmyResult, LemmyErrorType, MAX_COMMENT_DEPTH_LIMIT}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use std::ops::Deref; use url::Url; #[skip_serializing_none] @@ -58,9 +57,19 @@ impl Note { &self, context: &Data, ) -> LemmyResult<(ApubPost, Option)> { - // Fetch parent comment chain in a box, otherwise it can cause a stack overflow. - let parent = Box::pin(self.in_reply_to.dereference(context).await?); - match parent.deref() { + // We use recursion here to fetch the entire comment chain up to the top-level parent. This is + // necessary because we need to know the post and parent comment in order to insert a new + // comment. However it can also lead to stack overflow when fetching many comments recursively. + // To avoid this we check the request count against max comment depth, which based on testing + // can be handled without risking stack overflow. This is not a perfect solution, because in + // some cases we have to fetch user profiles too, and reach the limit after only 25 comments + // or so. + // A cleaner solution would be converting the recursion into a loop, but that is tricky. + if context.request_count() > MAX_COMMENT_DEPTH_LIMIT as u32 { + Err(LemmyErrorType::MaxCommentDepthReached)?; + } + let parent = self.in_reply_to.dereference(context).await?; + match parent { PostOrComment::Post(p) => Ok((p.clone(), None)), PostOrComment::Comment(c) => { let post_id = c.post_id; diff --git a/crates/db_perf/src/series.rs b/crates/db_perf/src/series.rs index b504efc54..8efc078b1 100644 --- a/crates/db_perf/src/series.rs +++ b/crates/db_perf/src/series.rs @@ -75,7 +75,7 @@ impl> ValidGrouping<()> type IsAggregate = is_aggregate::No; } -#[allow(non_camel_case_types)] +#[expect(non_camel_case_types)] #[derive(QueryId, Clone, Copy, Debug)] pub struct current_value; diff --git a/crates/db_schema/src/aggregates/comment_aggregates.rs b/crates/db_schema/src/aggregates/comment_aggregates.rs index 150899763..fc825ec99 100644 --- a/crates/db_schema/src/aggregates/comment_aggregates.rs +++ b/crates/db_schema/src/aggregates/comment_aggregates.rs @@ -30,8 +30,6 @@ impl CommentAggregates { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -46,26 +44,25 @@ mod tests { traits::{Crud, Likeable}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_comment_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_comment_agg"); - let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); + let another_inserted_person = Person::create(pool, &another_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -73,21 +70,21 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let new_post = PostInsertForm::new( "A test post".into(), inserted_person.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_person.id, inserted_post.id, "A test comment".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; let child_comment_form = CommentInsertForm::new( inserted_person.id, @@ -95,9 +92,7 @@ mod tests { "A test comment".into(), ); let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; let comment_like = CommentLikeForm { comment_id: inserted_comment.id, @@ -106,11 +101,9 @@ mod tests { score: 1, }; - CommentLike::like(pool, &comment_like).await.unwrap(); + CommentLike::like(pool, &comment_like).await?; - let comment_aggs_before_delete = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap(); + let comment_aggs_before_delete = CommentAggregates::read(pool, inserted_comment.id).await?; assert_eq!(1, comment_aggs_before_delete.score); assert_eq!(1, comment_aggs_before_delete.upvotes); @@ -124,47 +117,39 @@ mod tests { score: -1, }; - CommentLike::like(pool, &comment_dislike).await.unwrap(); + CommentLike::like(pool, &comment_dislike).await?; - let comment_aggs_after_dislike = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap(); + let comment_aggs_after_dislike = CommentAggregates::read(pool, inserted_comment.id).await?; assert_eq!(0, comment_aggs_after_dislike.score); assert_eq!(1, comment_aggs_after_dislike.upvotes); assert_eq!(1, comment_aggs_after_dislike.downvotes); // Remove the first comment like - CommentLike::remove(pool, inserted_person.id, inserted_comment.id) - .await - .unwrap(); - let after_like_remove = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap(); + CommentLike::remove(pool, inserted_person.id, inserted_comment.id).await?; + let after_like_remove = CommentAggregates::read(pool, inserted_comment.id).await?; assert_eq!(-1, after_like_remove.score); assert_eq!(0, after_like_remove.upvotes); assert_eq!(1, after_like_remove.downvotes); // Remove the parent post - Post::delete(pool, inserted_post.id).await.unwrap(); + Post::delete(pool, inserted_post.id).await?; // Should be none found, since the post was deleted let after_delete = CommentAggregates::read(pool, inserted_comment.id).await; assert!(after_delete.is_err()); // This should delete all the associated rows, and fire triggers - Person::delete(pool, another_inserted_person.id) - .await - .unwrap(); - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + Person::delete(pool, another_inserted_person.id).await?; + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/aggregates/community_aggregates.rs b/crates/db_schema/src/aggregates/community_aggregates.rs index 1e52052f6..0359d8632 100644 --- a/crates/db_schema/src/aggregates/community_aggregates.rs +++ b/crates/db_schema/src/aggregates/community_aggregates.rs @@ -1,6 +1,5 @@ use crate::{ aggregates::structs::CommunityAggregates, - diesel::OptionalExtension, newtypes::CommunityId, schema::{community_aggregates, community_aggregates::subscribers}, utils::{get_conn, DbPool}, @@ -9,16 +8,12 @@ use diesel::{result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; impl CommunityAggregates { - pub async fn read( - pool: &mut DbPool<'_>, - for_community_id: CommunityId, - ) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, for_community_id: CommunityId) -> Result { let conn = &mut get_conn(pool).await?; community_aggregates::table .find(for_community_id) .first(conn) .await - .optional() } pub async fn update_federated_followers( @@ -36,8 +31,6 @@ impl CommunityAggregates { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -52,26 +45,25 @@ mod tests { traits::{Crud, Followable}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); - let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); + let another_inserted_person = Person::create(pool, &another_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -79,7 +71,7 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let another_community = CommunityInsertForm::new( inserted_instance.id, @@ -87,7 +79,7 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let another_inserted_community = Community::create(pool, &another_community).await.unwrap(); + let another_inserted_community = Community::create(pool, &another_community).await?; let first_person_follow = CommunityFollowerForm { community_id: inserted_community.id, @@ -95,9 +87,7 @@ mod tests { pending: false, }; - CommunityFollower::follow(pool, &first_person_follow) - .await - .unwrap(); + CommunityFollower::follow(pool, &first_person_follow).await?; let second_person_follow = CommunityFollowerForm { community_id: inserted_community.id, @@ -105,9 +95,7 @@ mod tests { pending: false, }; - CommunityFollower::follow(pool, &second_person_follow) - .await - .unwrap(); + CommunityFollower::follow(pool, &second_person_follow).await?; let another_community_follow = CommunityFollowerForm { community_id: another_inserted_community.id, @@ -115,23 +103,21 @@ mod tests { pending: false, }; - CommunityFollower::follow(pool, &another_community_follow) - .await - .unwrap(); + CommunityFollower::follow(pool, &another_community_follow).await?; let new_post = PostInsertForm::new( "A test post".into(), inserted_person.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_person.id, inserted_post.id, "A test comment".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; let child_comment_form = CommentInsertForm::new( inserted_person.id, @@ -139,14 +125,10 @@ mod tests { "A test comment".into(), ); let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - let community_aggregates_before_delete = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + let community_aggregates_before_delete = + CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(2, community_aggregates_before_delete.subscribers); assert_eq!(2, community_aggregates_before_delete.subscribers_local); @@ -154,76 +136,53 @@ mod tests { assert_eq!(2, community_aggregates_before_delete.comments); // Test the other community - let another_community_aggs = CommunityAggregates::read(pool, another_inserted_community.id) - .await - .unwrap() - .unwrap(); + let another_community_aggs = + CommunityAggregates::read(pool, another_inserted_community.id).await?; assert_eq!(1, another_community_aggs.subscribers); assert_eq!(1, another_community_aggs.subscribers_local); assert_eq!(0, another_community_aggs.posts); assert_eq!(0, another_community_aggs.comments); // Unfollow test - CommunityFollower::unfollow(pool, &second_person_follow) - .await - .unwrap(); - let after_unfollow = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + CommunityFollower::unfollow(pool, &second_person_follow).await?; + let after_unfollow = CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(1, after_unfollow.subscribers); assert_eq!(1, after_unfollow.subscribers_local); // Follow again just for the later tests - CommunityFollower::follow(pool, &second_person_follow) - .await - .unwrap(); - let after_follow_again = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + CommunityFollower::follow(pool, &second_person_follow).await?; + let after_follow_again = CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(2, after_follow_again.subscribers); assert_eq!(2, after_follow_again.subscribers_local); // Remove a parent post (the comment count should also be 0) - Post::delete(pool, inserted_post.id).await.unwrap(); - let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + Post::delete(pool, inserted_post.id).await?; + let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(0, after_parent_post_delete.comments); assert_eq!(0, after_parent_post_delete.posts); // Remove the 2nd person - Person::delete(pool, another_inserted_person.id) - .await - .unwrap(); - let after_person_delete = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + Person::delete(pool, another_inserted_person.id).await?; + let after_person_delete = CommunityAggregates::read(pool, inserted_community.id).await?; assert_eq!(1, after_person_delete.subscribers); assert_eq!(1, after_person_delete.subscribers_local); // This should delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); - let another_community_num_deleted = Community::delete(pool, another_inserted_community.id) - .await - .unwrap(); + let another_community_num_deleted = + Community::delete(pool, another_inserted_community.id).await?; assert_eq!(1, another_community_num_deleted); // Should be none found, since the creator was deleted - let after_delete = CommunityAggregates::read(pool, inserted_community.id) - .await - .unwrap(); - assert!(after_delete.is_none()); + let after_delete = CommunityAggregates::read(pool, inserted_community.id).await; + assert!(after_delete.is_err()); + + Ok(()) } } diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index 337af5ef3..c68c90b0d 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -1,4 +1,3 @@ -pub(crate) use crate::diesel::OptionalExtension; use crate::{ aggregates::structs::PersonAggregates, newtypes::PersonId, @@ -9,19 +8,13 @@ use diesel::{result::Error, QueryDsl}; use diesel_async::RunQueryDsl; impl PersonAggregates { - pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { let conn = &mut get_conn(pool).await?; - person_aggregates::table - .find(person_id) - .first(conn) - .await - .optional() + person_aggregates::table.find(person_id).first(conn).await } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -36,26 +29,25 @@ mod tests { traits::{Crud, Likeable}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_user_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_user_agg"); - let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); + let another_inserted_person = Person::create(pool, &another_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -64,28 +56,28 @@ mod tests { "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let new_post = PostInsertForm::new( "A test post".into(), inserted_person.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let post_like = PostLikeForm { post_id: inserted_post.id, person_id: inserted_person.id, score: 1, }; - let _inserted_post_like = PostLike::like(pool, &post_like).await.unwrap(); + let _inserted_post_like = PostLike::like(pool, &post_like).await?; let comment_form = CommentInsertForm::new( inserted_person.id, inserted_post.id, "A test comment".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; let mut comment_like = CommentLikeForm { comment_id: inserted_comment.id, @@ -94,7 +86,7 @@ mod tests { score: 1, }; - let _inserted_comment_like = CommentLike::like(pool, &comment_like).await.unwrap(); + let _inserted_comment_like = CommentLike::like(pool, &comment_like).await?; let child_comment_form = CommentInsertForm::new( inserted_person.id, @@ -102,9 +94,7 @@ mod tests { "A test comment".into(), ); let inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; let child_comment_like = CommentLikeForm { comment_id: inserted_child_comment.id, @@ -113,12 +103,9 @@ mod tests { score: 1, }; - let _inserted_child_comment_like = CommentLike::like(pool, &child_comment_like).await.unwrap(); + let _inserted_child_comment_like = CommentLike::like(pool, &child_comment_like).await?; - let person_aggregates_before_delete = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + let person_aggregates_before_delete = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(1, person_aggregates_before_delete.post_count); assert_eq!(1, person_aggregates_before_delete.post_score); @@ -126,13 +113,8 @@ mod tests { assert_eq!(2, person_aggregates_before_delete.comment_score); // Remove a post like - PostLike::remove(pool, inserted_person.id, inserted_post.id) - .await - .unwrap(); - let after_post_like_remove = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; + let after_post_like_remove = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(0, after_post_like_remove.post_score); Comment::update( @@ -143,8 +125,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; Comment::update( pool, inserted_child_comment.id, @@ -153,51 +134,34 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let after_parent_comment_removed = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + let after_parent_comment_removed = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(0, after_parent_comment_removed.comment_count); // TODO: fix person aggregate comment score calculation // assert_eq!(0, after_parent_comment_removed.comment_score); // Remove a parent comment (the scores should also be removed) - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Comment::delete(pool, inserted_child_comment.id) - .await - .unwrap(); - let after_parent_comment_delete = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + Comment::delete(pool, inserted_comment.id).await?; + Comment::delete(pool, inserted_child_comment.id).await?; + let after_parent_comment_delete = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(0, after_parent_comment_delete.comment_count); // TODO: fix person aggregate comment score calculation // assert_eq!(0, after_parent_comment_delete.comment_score); // Add in the two comments again, then delete the post. - let new_parent_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let new_parent_comment = Comment::create(pool, &comment_form, None).await?; let _new_child_comment = - Comment::create(pool, &child_comment_form, Some(&new_parent_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&new_parent_comment.path)).await?; comment_like.comment_id = new_parent_comment.id; - CommentLike::like(pool, &comment_like).await.unwrap(); - let after_comment_add = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + CommentLike::like(pool, &comment_like).await?; + let after_comment_add = PersonAggregates::read(pool, inserted_person.id).await?; assert_eq!(2, after_comment_add.comment_count); // TODO: fix person aggregate comment score calculation // assert_eq!(1, after_comment_add.comment_score); - Post::delete(pool, inserted_post.id).await.unwrap(); - let after_post_delete = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + Post::delete(pool, inserted_post.id).await?; + let after_post_delete = PersonAggregates::read(pool, inserted_person.id).await?; // TODO: fix person aggregate comment score calculation // assert_eq!(0, after_post_delete.comment_score); assert_eq!(0, after_post_delete.comment_count); @@ -205,24 +169,20 @@ mod tests { assert_eq!(0, after_post_delete.post_count); // This should delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); - Person::delete(pool, another_inserted_person.id) - .await - .unwrap(); + Person::delete(pool, another_inserted_person.id).await?; // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); // Should be none found - let after_delete = PersonAggregates::read(pool, inserted_person.id) - .await - .unwrap(); - assert!(after_delete.is_none()); + let after_delete = PersonAggregates::read(pool, inserted_person.id).await; + assert!(after_delete.is_err()); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/aggregates/post_aggregates.rs b/crates/db_schema/src/aggregates/post_aggregates.rs index f15567d8f..b63017317 100644 --- a/crates/db_schema/src/aggregates/post_aggregates.rs +++ b/crates/db_schema/src/aggregates/post_aggregates.rs @@ -49,8 +49,6 @@ impl PostAggregates { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -65,26 +63,25 @@ mod tests { traits::{Crud, Likeable}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg"); - let another_inserted_person = Person::create(pool, &another_person).await.unwrap(); + let another_inserted_person = Person::create(pool, &another_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -92,21 +89,21 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let new_post = PostInsertForm::new( "A test post".into(), inserted_person.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_person.id, inserted_post.id, "A test comment".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; let child_comment_form = CommentInsertForm::new( inserted_person.id, @@ -114,9 +111,7 @@ mod tests { "A test comment".into(), ); let inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; let post_like = PostLikeForm { post_id: inserted_post.id, @@ -124,9 +119,9 @@ mod tests { score: 1, }; - PostLike::like(pool, &post_like).await.unwrap(); + PostLike::like(pool, &post_like).await?; - let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(2, post_aggs_before_delete.comments); assert_eq!(1, post_aggs_before_delete.score); @@ -140,9 +135,9 @@ mod tests { score: -1, }; - PostLike::like(pool, &post_dislike).await.unwrap(); + PostLike::like(pool, &post_dislike).await?; - let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(2, post_aggs_after_dislike.comments); assert_eq!(0, post_aggs_after_dislike.score); @@ -150,59 +145,51 @@ mod tests { assert_eq!(1, post_aggs_after_dislike.downvotes); // Remove the comments - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Comment::delete(pool, inserted_child_comment.id) - .await - .unwrap(); - let after_comment_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + Comment::delete(pool, inserted_comment.id).await?; + Comment::delete(pool, inserted_child_comment.id).await?; + let after_comment_delete = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, after_comment_delete.comments); assert_eq!(0, after_comment_delete.score); assert_eq!(1, after_comment_delete.upvotes); assert_eq!(1, after_comment_delete.downvotes); // Remove the first post like - PostLike::remove(pool, inserted_person.id, inserted_post.id) - .await - .unwrap(); - let after_like_remove = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; + let after_like_remove = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, after_like_remove.comments); assert_eq!(-1, after_like_remove.score); assert_eq!(0, after_like_remove.upvotes); assert_eq!(1, after_like_remove.downvotes); // This should delete all the associated rows, and fire triggers - Person::delete(pool, another_inserted_person.id) - .await - .unwrap(); - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + Person::delete(pool, another_inserted_person.id).await?; + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); // Should be none found, since the creator was deleted let after_delete = PostAggregates::read(pool, inserted_post.id).await; assert!(after_delete.is_err()); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_soft_delete() { + async fn test_soft_delete() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -210,14 +197,14 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let new_post = PostInsertForm::new( "A test post".into(), inserted_person.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_person.id, @@ -225,9 +212,9 @@ mod tests { "A test comment".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; - let post_aggregates_before = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggregates_before = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(1, post_aggregates_before.comments); Comment::update( @@ -238,10 +225,9 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, post_aggregates_after_remove.comments); Comment::update( @@ -252,8 +238,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; Comment::update( pool, @@ -263,10 +248,9 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, post_aggregates_after_delete.comments); Comment::update( @@ -277,19 +261,17 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let post_aggregates_after_delete_remove = - PostAggregates::read(pool, inserted_post.id).await.unwrap(); + let post_aggregates_after_delete_remove = PostAggregates::read(pool, inserted_post.id).await?; assert_eq!(0, post_aggregates_after_delete_remove.comments); - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Post::delete(pool, inserted_post.id).await.unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Comment::delete(pool, inserted_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Person::delete(pool, inserted_person.id).await?; + Community::delete(pool, inserted_community.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index 81d6b559d..379ddd2d9 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -1,6 +1,5 @@ use crate::{ aggregates::structs::SiteAggregates, - diesel::OptionalExtension, schema::site_aggregates, utils::{get_conn, DbPool}, }; @@ -8,15 +7,13 @@ use diesel::result::Error; use diesel_async::RunQueryDsl; impl SiteAggregates { - pub async fn read(pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn read(pool: &mut DbPool<'_>) -> Result { let conn = &mut get_conn(pool).await?; - site_aggregates::table.first(conn).await.optional() + site_aggregates::table.first(conn).await } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -32,22 +29,21 @@ mod tests { traits::Crud, utils::{build_db_pool_for_tests, DbPool}, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; async fn prepare_site_with_community( pool: &mut DbPool<'_>, - ) -> (Instance, Person, Site, Community) { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + ) -> Result<(Instance, Person, Site, Community), Error> { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_site_agg"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let site_form = SiteInsertForm::new("test_site".into(), inserted_instance.id); - let inserted_site = Site::create(pool, &site_form).await.unwrap(); + let inserted_site = Site::create(pool, &site_form).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -56,23 +52,24 @@ mod tests { "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); - ( + let inserted_community = Community::create(pool, &new_community).await?; + + Ok(( inserted_instance, inserted_person, inserted_site, inserted_community, - ) + )) } #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); let (inserted_instance, inserted_person, inserted_site, inserted_community) = - prepare_site_with_community(pool).await; + prepare_site_with_community(pool).await?; let new_post = PostInsertForm::new( "A test post".into(), @@ -81,8 +78,8 @@ mod tests { ); // Insert two of those posts - let inserted_post = Post::create(pool, &new_post).await.unwrap(); - let _inserted_post_again = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; + let _inserted_post_again = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_person.id, @@ -91,7 +88,7 @@ mod tests { ); // Insert two of those comments - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; let child_comment_form = CommentInsertForm::new( inserted_person.id, @@ -99,11 +96,9 @@ mod tests { "A test comment".into(), ); let _inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; - let site_aggregates_before_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_before_delete = SiteAggregates::read(pool).await?; // TODO: this is unstable, sometimes it returns 0 users, sometimes 1 //assert_eq!(0, site_aggregates_before_delete.users); @@ -112,42 +107,42 @@ mod tests { assert_eq!(2, site_aggregates_before_delete.comments); // Try a post delete - Post::delete(pool, inserted_post.id).await.unwrap(); - let site_aggregates_after_post_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); + Post::delete(pool, inserted_post.id).await?; + let site_aggregates_after_post_delete = SiteAggregates::read(pool).await?; assert_eq!(1, site_aggregates_after_post_delete.posts); assert_eq!(0, site_aggregates_after_post_delete.comments); // This shouuld delete all the associated rows, and fire triggers - let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let person_num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, person_num_deleted); // Delete the community - let community_num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); + let community_num_deleted = Community::delete(pool, inserted_community.id).await?; assert_eq!(1, community_num_deleted); // Site should still exist, it can without a site creator. let after_delete_creator = SiteAggregates::read(pool).await; assert!(after_delete_creator.is_ok()); - Site::delete(pool, inserted_site.id).await.unwrap(); - let after_delete_site = SiteAggregates::read(pool).await.unwrap(); - assert!(after_delete_site.is_none()); + Site::delete(pool, inserted_site.id).await?; + let after_delete_site = SiteAggregates::read(pool).await; + assert!(after_delete_site.is_err()); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_soft_delete() { + async fn test_soft_delete() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); let (inserted_instance, inserted_person, inserted_site, inserted_community) = - prepare_site_with_community(pool).await; + prepare_site_with_community(pool).await?; - let site_aggregates_before = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_before = SiteAggregates::read(pool).await?; assert_eq!(1, site_aggregates_before.communities); Community::update( @@ -158,10 +153,9 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let site_aggregates_after_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_after_delete = SiteAggregates::read(pool).await?; assert_eq!(0, site_aggregates_after_delete.communities); Community::update( @@ -172,8 +166,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; Community::update( pool, @@ -183,10 +176,9 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let site_aggregates_after_remove = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_after_remove = SiteAggregates::read(pool).await?; assert_eq!(0, site_aggregates_after_remove.communities); Community::update( @@ -197,17 +189,16 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; - let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await.unwrap().unwrap(); + let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await?; assert_eq!(0, site_aggregates_after_remove_delete.communities); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Site::delete(pool, inserted_site.id).await.unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Community::delete(pool, inserted_community.id).await?; + Site::delete(pool, inserted_site.id).await?; + Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/impls/activity.rs b/crates/db_schema/src/impls/activity.rs index 6b057d254..fff0c2f0c 100644 --- a/crates/db_schema/src/impls/activity.rs +++ b/crates/db_schema/src/impls/activity.rs @@ -58,12 +58,11 @@ impl ReceivedActivity { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; use crate::{source::activity::ActorType, utils::build_db_pool_for_tests}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serde_json::json; use serial_test::serial; @@ -71,26 +70,25 @@ mod tests { #[tokio::test] #[serial] - async fn receive_activity_duplicate() { + async fn receive_activity_duplicate() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let ap_id: DbUrl = Url::parse("http://example.com/activity/531") - .unwrap() - .into(); + let ap_id: DbUrl = Url::parse("http://example.com/activity/531")?.into(); // inserting activity should only work once - ReceivedActivity::create(pool, &ap_id).await.unwrap(); - ReceivedActivity::create(pool, &ap_id).await.unwrap_err(); + ReceivedActivity::create(pool, &ap_id).await?; + let second = ReceivedActivity::create(pool, &ap_id).await; + assert!(second.is_err()); + + Ok(()) } #[tokio::test] #[serial] - async fn sent_activity_write_read() { + async fn sent_activity_write_read() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let ap_id: DbUrl = Url::parse("http://example.com/activity/412") - .unwrap() - .into(); + let ap_id: DbUrl = Url::parse("http://example.com/activity/412")?.into(); let data = json!({ "key1": "0xF9BA143B95FF6D82", "key2": "42", @@ -101,20 +99,20 @@ mod tests { ap_id: ap_id.clone(), data: data.clone(), sensitive, - actor_apub_id: Url::parse("http://example.com/u/exampleuser") - .unwrap() - .into(), + actor_apub_id: Url::parse("http://example.com/u/exampleuser")?.into(), actor_type: ActorType::Person, send_all_instances: false, send_community_followers_of: None, send_inboxes: vec![], }; - SentActivity::create(pool, form).await.unwrap(); + SentActivity::create(pool, form).await?; - let res = SentActivity::read_from_apub_id(pool, &ap_id).await.unwrap(); + let res = SentActivity::read_from_apub_id(pool, &ap_id).await?; assert_eq!(res.ap_id, ap_id); assert_eq!(res.data, data); assert_eq!(res.sensitive, sensitive); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index 958c5b1e6..bff729f41 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -199,26 +199,22 @@ impl CommunityLanguage { /// Returns true if the given language is one of configured languages for given community pub async fn is_allowed_community_language( pool: &mut DbPool<'_>, - for_language_id: Option, + for_language_id: LanguageId, for_community_id: CommunityId, ) -> LemmyResult<()> { use crate::schema::community_language::dsl::community_language; let conn = &mut get_conn(pool).await?; - if let Some(for_language_id) = for_language_id { - let is_allowed = select(exists( - community_language.find((for_community_id, for_language_id)), - )) - .get_result(conn) - .await?; + let is_allowed = select(exists( + community_language.find((for_community_id, for_language_id)), + )) + .get_result(conn) + .await?; - if is_allowed { - Ok(()) - } else { - Err(LemmyErrorType::LanguageNotAllowed)? - } - } else { + if is_allowed { Ok(()) + } else { + Err(LemmyErrorType::LanguageNotAllowed)? } } @@ -327,7 +323,7 @@ pub async fn default_post_language( pool: &mut DbPool<'_>, community_id: CommunityId, local_user_id: LocalUserId, -) -> Result, Error> { +) -> Result { use crate::schema::{community_language::dsl as cl, local_user_language::dsl as ul}; let conn = &mut get_conn(pool).await?; let mut intersection = ul::local_user_language @@ -339,12 +335,12 @@ pub async fn default_post_language( .await?; if intersection.len() == 1 { - Ok(intersection.pop()) + Ok(intersection.pop().unwrap_or(UNDETERMINED_ID)) } else if intersection.len() == 2 && intersection.contains(&UNDETERMINED_ID) { intersection.retain(|i| i != &UNDETERMINED_ID); - Ok(intersection.pop()) + Ok(intersection.pop().unwrap_or(UNDETERMINED_ID)) } else { - Ok(None) + Ok(UNDETERMINED_ID) } } @@ -392,8 +388,7 @@ async fn convert_read_languages( } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use super::*; @@ -409,168 +404,148 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; - async fn test_langs1(pool: &mut DbPool<'_>) -> Vec { - vec![ - Language::read_id_from_code(pool, Some("en")) - .await - .unwrap() - .unwrap(), - Language::read_id_from_code(pool, Some("fr")) - .await - .unwrap() - .unwrap(), - Language::read_id_from_code(pool, Some("ru")) - .await - .unwrap() - .unwrap(), - ] + async fn test_langs1(pool: &mut DbPool<'_>) -> Result, Error> { + Ok(vec![ + Language::read_id_from_code(pool, "en").await?, + Language::read_id_from_code(pool, "fr").await?, + Language::read_id_from_code(pool, "ru").await?, + ]) } - async fn test_langs2(pool: &mut DbPool<'_>) -> Vec { - vec![ - Language::read_id_from_code(pool, Some("fi")) - .await - .unwrap() - .unwrap(), - Language::read_id_from_code(pool, Some("se")) - .await - .unwrap() - .unwrap(), - ] + async fn test_langs2(pool: &mut DbPool<'_>) -> Result, Error> { + Ok(vec![ + Language::read_id_from_code(pool, "fi").await?, + Language::read_id_from_code(pool, "se").await?, + ]) } - async fn create_test_site(pool: &mut DbPool<'_>) -> (Site, Instance) { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + async fn create_test_site(pool: &mut DbPool<'_>) -> Result<(Site, Instance), Error> { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id); - let site = Site::create(pool, &site_form).await.unwrap(); + let site = Site::create(pool, &site_form).await?; // Create a local site, since this is necessary for local languages let local_site_form = LocalSiteInsertForm::new(site.id); - LocalSite::create(pool, &local_site_form).await.unwrap(); + LocalSite::create(pool, &local_site_form).await?; - (site, inserted_instance) + Ok((site, inserted_instance)) } #[tokio::test] #[serial] - async fn test_convert_update_languages() { + async fn test_convert_update_languages() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); // call with empty vec, returns all languages - let conn = &mut get_conn(pool).await.unwrap(); - let converted1 = convert_update_languages(conn, vec![]).await.unwrap(); + let conn = &mut get_conn(pool).await?; + let converted1 = convert_update_languages(conn, vec![]).await?; assert_eq!(184, converted1.len()); // call with nonempty vec, returns same vec - let test_langs = test_langs1(&mut conn.into()).await; - let converted2 = convert_update_languages(conn, test_langs.clone()) - .await - .unwrap(); + let test_langs = test_langs1(&mut conn.into()).await?; + let converted2 = convert_update_languages(conn, test_langs.clone()).await?; assert_eq!(test_langs, converted2); + + Ok(()) } #[tokio::test] #[serial] - async fn test_convert_read_languages() { + async fn test_convert_read_languages() -> Result<(), Error> { use crate::schema::language::dsl::{id, language}; let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); // call with all languages, returns empty vec - let conn = &mut get_conn(pool).await.unwrap(); - let all_langs = language.select(id).get_results(conn).await.unwrap(); - let converted1: Vec = convert_read_languages(conn, all_langs).await.unwrap(); + let conn = &mut get_conn(pool).await?; + let all_langs = language.select(id).get_results(conn).await?; + let converted1: Vec = convert_read_languages(conn, all_langs).await?; assert_eq!(0, converted1.len()); // call with nonempty vec, returns same vec - let test_langs = test_langs1(&mut conn.into()).await; - let converted2 = convert_read_languages(conn, test_langs.clone()) - .await - .unwrap(); + let test_langs = test_langs1(&mut conn.into()).await?; + let converted2 = convert_read_languages(conn, test_langs.clone()).await?; assert_eq!(test_langs, converted2); + + Ok(()) } #[tokio::test] #[serial] - async fn test_site_languages() { + async fn test_site_languages() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let (site, instance) = create_test_site(pool).await; - let site_languages1 = SiteLanguage::read_local_raw(pool).await.unwrap(); + let (site, instance) = create_test_site(pool).await?; + let site_languages1 = SiteLanguage::read_local_raw(pool).await?; // site is created with all languages assert_eq!(184, site_languages1.len()); - let test_langs = test_langs1(pool).await; - SiteLanguage::update(pool, test_langs.clone(), &site) - .await - .unwrap(); + let test_langs = test_langs1(pool).await?; + SiteLanguage::update(pool, test_langs.clone(), &site).await?; - let site_languages2 = SiteLanguage::read_local_raw(pool).await.unwrap(); + let site_languages2 = SiteLanguage::read_local_raw(pool).await?; // after update, site only has new languages assert_eq!(test_langs, site_languages2); - Site::delete(pool, site.id).await.unwrap(); - Instance::delete(pool, instance.id).await.unwrap(); - LocalSite::delete(pool).await.unwrap(); + Site::delete(pool, site.id).await?; + Instance::delete(pool, instance.id).await?; + LocalSite::delete(pool).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_user_languages() { + async fn test_user_languages() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let (site, instance) = create_test_site(pool).await; + let (site, instance) = create_test_site(pool).await?; let person_form = PersonInsertForm::test_form(instance.id, "my test person"); - let person = Person::create(pool, &person_form).await.unwrap(); + let person = Person::create(pool, &person_form).await?; let local_user_form = LocalUserInsertForm::test_form(person.id); - let local_user = LocalUser::create(pool, &local_user_form, vec![]) - .await - .unwrap(); - let local_user_langs1 = LocalUserLanguage::read(pool, local_user.id).await.unwrap(); + let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; + let local_user_langs1 = LocalUserLanguage::read(pool, local_user.id).await?; // new user should be initialized with all languages assert_eq!(0, local_user_langs1.len()); // update user languages - let test_langs2 = test_langs2(pool).await; - LocalUserLanguage::update(pool, test_langs2, local_user.id) - .await - .unwrap(); - let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await.unwrap(); + let test_langs2 = test_langs2(pool).await?; + LocalUserLanguage::update(pool, test_langs2, local_user.id).await?; + let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await?; assert_eq!(3, local_user_langs2.len()); - Person::delete(pool, person.id).await.unwrap(); - LocalUser::delete(pool, local_user.id).await.unwrap(); - Site::delete(pool, site.id).await.unwrap(); - LocalSite::delete(pool).await.unwrap(); - Instance::delete(pool, instance.id).await.unwrap(); + Person::delete(pool, person.id).await?; + LocalUser::delete(pool, local_user.id).await?; + Site::delete(pool, site.id).await?; + LocalSite::delete(pool).await?; + Instance::delete(pool, instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_community_languages() { + async fn test_community_languages() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let (site, instance) = create_test_site(pool).await; - let test_langs = test_langs1(pool).await; - SiteLanguage::update(pool, test_langs.clone(), &site) - .await - .unwrap(); + let (site, instance) = create_test_site(pool).await?; + let test_langs = test_langs1(pool).await?; + SiteLanguage::update(pool, test_langs.clone(), &site).await?; - let read_site_langs = SiteLanguage::read(pool, site.id).await.unwrap(); + let read_site_langs = SiteLanguage::read(pool, site.id).await?; assert_eq!(test_langs, read_site_langs); // Test the local ones are the same - let read_local_site_langs = SiteLanguage::read_local_raw(pool).await.unwrap(); + let read_local_site_langs = SiteLanguage::read_local_raw(pool).await?; assert_eq!(test_langs, read_local_site_langs); let community_form = CommunityInsertForm::new( @@ -579,52 +554,48 @@ mod tests { "test community".to_string(), "pubkey".to_string(), ); - let community = Community::create(pool, &community_form).await.unwrap(); - let community_langs1 = CommunityLanguage::read(pool, community.id).await.unwrap(); + let community = Community::create(pool, &community_form).await?; + let community_langs1 = CommunityLanguage::read(pool, community.id).await?; // community is initialized with site languages assert_eq!(test_langs, community_langs1); let allowed_lang1 = - CommunityLanguage::is_allowed_community_language(pool, Some(test_langs[0]), community.id) - .await; + CommunityLanguage::is_allowed_community_language(pool, test_langs[0], community.id).await; assert!(allowed_lang1.is_ok()); - let test_langs2 = test_langs2(pool).await; + let test_langs2 = test_langs2(pool).await?; let allowed_lang2 = - CommunityLanguage::is_allowed_community_language(pool, Some(test_langs2[0]), community.id) - .await; + CommunityLanguage::is_allowed_community_language(pool, test_langs2[0], community.id).await; assert!(allowed_lang2.is_err()); // limit site languages to en, fi. after this, community languages should be updated to // intersection of old languages (en, fr, ru) and (en, fi), which is only fi. - SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &site) - .await - .unwrap(); - let community_langs2 = CommunityLanguage::read(pool, community.id).await.unwrap(); + SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &site).await?; + let community_langs2 = CommunityLanguage::read(pool, community.id).await?; assert_eq!(vec![test_langs[0]], community_langs2); // update community languages to different ones - CommunityLanguage::update(pool, test_langs2.clone(), community.id) - .await - .unwrap(); - let community_langs3 = CommunityLanguage::read(pool, community.id).await.unwrap(); + CommunityLanguage::update(pool, test_langs2.clone(), community.id).await?; + let community_langs3 = CommunityLanguage::read(pool, community.id).await?; assert_eq!(test_langs2, community_langs3); - Community::delete(pool, community.id).await.unwrap(); - Site::delete(pool, site.id).await.unwrap(); - LocalSite::delete(pool).await.unwrap(); - Instance::delete(pool, instance.id).await.unwrap(); + Community::delete(pool, community.id).await?; + Site::delete(pool, site.id).await?; + LocalSite::delete(pool).await?; + Instance::delete(pool, instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_default_post_language() { + async fn test_default_post_language() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let (site, instance) = create_test_site(pool).await; - let test_langs = test_langs1(pool).await; - let test_langs2 = test_langs2(pool).await; + let (site, instance) = create_test_site(pool).await?; + let test_langs = test_langs1(pool).await?; + let test_langs2 = test_langs2(pool).await?; let community_form = CommunityInsertForm::new( instance.id, @@ -632,58 +603,39 @@ mod tests { "test community".to_string(), "pubkey".to_string(), ); - let community = Community::create(pool, &community_form).await.unwrap(); - CommunityLanguage::update(pool, test_langs, community.id) - .await - .unwrap(); + let community = Community::create(pool, &community_form).await?; + CommunityLanguage::update(pool, test_langs, community.id).await?; let person_form = PersonInsertForm::test_form(instance.id, "my test person"); - let person = Person::create(pool, &person_form).await.unwrap(); + let person = Person::create(pool, &person_form).await?; let local_user_form = LocalUserInsertForm::test_form(person.id); - let local_user = LocalUser::create(pool, &local_user_form, vec![]) - .await - .unwrap(); - LocalUserLanguage::update(pool, test_langs2, local_user.id) - .await - .unwrap(); + let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; + LocalUserLanguage::update(pool, test_langs2, local_user.id).await?; // no overlap in user/community languages, so defaults to undetermined - let def1 = default_post_language(pool, community.id, local_user.id) - .await - .unwrap(); - assert_eq!(None, def1); + let def1 = default_post_language(pool, community.id, local_user.id).await?; + assert_eq!(UNDETERMINED_ID, def1); - let ru = Language::read_id_from_code(pool, Some("ru")) - .await - .unwrap() - .unwrap(); + let ru = Language::read_id_from_code(pool, "ru").await?; let test_langs3 = vec![ ru, - Language::read_id_from_code(pool, Some("fi")) - .await - .unwrap() - .unwrap(), - Language::read_id_from_code(pool, Some("se")) - .await - .unwrap() - .unwrap(), + Language::read_id_from_code(pool, "fi").await?, + Language::read_id_from_code(pool, "se").await?, UNDETERMINED_ID, ]; - LocalUserLanguage::update(pool, test_langs3, local_user.id) - .await - .unwrap(); + LocalUserLanguage::update(pool, test_langs3, local_user.id).await?; // this time, both have ru as common lang - let def2 = default_post_language(pool, community.id, local_user.id) - .await - .unwrap(); - assert_eq!(Some(ru), def2); + let def2 = default_post_language(pool, community.id, local_user.id).await?; + assert_eq!(ru, def2); - Person::delete(pool, person.id).await.unwrap(); - Community::delete(pool, community.id).await.unwrap(); - LocalUser::delete(pool, local_user.id).await.unwrap(); - Site::delete(pool, site.id).await.unwrap(); - LocalSite::delete(pool).await.unwrap(); - Instance::delete(pool, instance.id).await.unwrap(); + Person::delete(pool, person.id).await?; + Community::delete(pool, community.id).await?; + LocalUser::delete(pool, local_user.id).await?; + Site::delete(pool, site.id).await?; + LocalSite::delete(pool).await?; + Instance::delete(pool, instance.id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/impls/captcha_answer.rs b/crates/db_schema/src/impls/captcha_answer.rs index 976d89515..d7183e4fb 100644 --- a/crates/db_schema/src/impls/captcha_answer.rs +++ b/crates/db_schema/src/impls/captcha_answer.rs @@ -51,8 +51,6 @@ impl CaptchaAnswer { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 0a4d7dd0e..ec246c121 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -40,12 +40,12 @@ impl Comment { pub async fn update_removed_for_creator( pool: &mut DbPool<'_>, for_creator_id: PersonId, - new_removed: bool, + removed: bool, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; diesel::update(comment::table.filter(comment::creator_id.eq(for_creator_id))) .set(( - comment::removed.eq(new_removed), + comment::removed.eq(removed), comment::updated.eq(naive_now()), )) .get_results::(conn) @@ -196,8 +196,6 @@ impl Saveable for CommentSaved { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -221,23 +219,22 @@ mod tests { utils::build_db_pool_for_tests, }; use diesel_ltree::Ltree; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; use url::Url; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "terry"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -245,21 +242,21 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let new_post = PostInsertForm::new( "A test post".into(), inserted_person.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_person.id, inserted_post.id, "A test comment".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; let expected_comment = Comment { id: inserted_comment.id, @@ -274,8 +271,7 @@ mod tests { ap_id: Url::parse(&format!( "https://lemmy-alpha/comment/{}", inserted_comment.id - )) - .unwrap() + ))? .into(), distinguished: false, local: true, @@ -288,9 +284,7 @@ mod tests { "A child comment".into(), ); let inserted_child_comment = - Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)) - .await - .unwrap(); + Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?; // Comment Like let comment_like_form = CommentLikeForm { @@ -300,7 +294,7 @@ mod tests { score: 1, }; - let inserted_comment_like = CommentLike::like(pool, &comment_like_form).await.unwrap(); + let inserted_comment_like = CommentLike::like(pool, &comment_like_form).await?; let expected_comment_like = CommentLike { comment_id: inserted_comment.id, @@ -316,7 +310,7 @@ mod tests { person_id: inserted_person.id, }; - let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await.unwrap(); + let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await?; let expected_comment_saved = CommentSaved { comment_id: inserted_comment.id, @@ -329,27 +323,17 @@ mod tests { ..Default::default() }; - let updated_comment = Comment::update(pool, inserted_comment.id, &comment_update_form) - .await - .unwrap(); + let updated_comment = Comment::update(pool, inserted_comment.id, &comment_update_form).await?; - let read_comment = Comment::read(pool, inserted_comment.id).await.unwrap(); - let like_removed = CommentLike::remove(pool, inserted_person.id, inserted_comment.id) - .await - .unwrap(); - let saved_removed = CommentSaved::unsave(pool, &comment_saved_form) - .await - .unwrap(); - let num_deleted = Comment::delete(pool, inserted_comment.id).await.unwrap(); - Comment::delete(pool, inserted_child_comment.id) - .await - .unwrap(); - Post::delete(pool, inserted_post.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let read_comment = Comment::read(pool, inserted_comment.id).await?; + let like_removed = CommentLike::remove(pool, inserted_person.id, inserted_comment.id).await?; + let saved_removed = CommentSaved::unsave(pool, &comment_saved_form).await?; + let num_deleted = Comment::delete(pool, inserted_comment.id).await?; + Comment::delete(pool, inserted_child_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_comment, read_comment); assert_eq!(expected_comment, inserted_comment); @@ -363,5 +347,7 @@ mod tests { assert_eq!(1, like_removed); assert_eq!(1, saved_removed); assert_eq!(1, num_deleted); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index f106ca424..a054059a4 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -30,12 +30,13 @@ use crate::{ get_conn, DbPool, }, + ListingType, SubscribedType, }; use chrono::{DateTime, Utc}; use diesel::{ deserialize, - dsl::{self, exists, insert_into}, + dsl::{self, exists, insert_into, not}, pg::Pg, result::Error, select, @@ -193,6 +194,30 @@ impl Community { .await?; Ok(()) } + + pub async fn get_random_community_id( + pool: &mut DbPool<'_>, + type_: &Option, + ) -> Result { + let conn = &mut get_conn(pool).await?; + sql_function!(fn random() -> Text); + + let mut query = community::table + .filter(not(community::deleted)) + .filter(not(community::removed)) + .into_boxed(); + + if let Some(ListingType::Local) = type_ { + query = query.filter(community::local); + } + + query + .select(community::id) + .order(random()) + .limit(1) + .first::(conn) + .await + } } impl CommunityModerator { @@ -431,7 +456,6 @@ impl ApubActor for Community { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ source::{ diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index 80408a526..cbfd14b03 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -48,20 +48,19 @@ impl FederationAllowList { } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ source::{federation_allowlist::FederationAllowList, instance::Instance}, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_allowlist_insert_and_clear() { + async fn test_allowlist_insert_and_clear() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); let domains = vec![ @@ -72,9 +71,9 @@ mod tests { let allowed = Some(domains.clone()); - FederationAllowList::replace(pool, allowed).await.unwrap(); + FederationAllowList::replace(pool, allowed).await?; - let allows = Instance::allowlist(pool).await.unwrap(); + let allows = Instance::allowlist(pool).await?; let allows_domains = allows .iter() .map(|i| i.domain.clone()) @@ -86,13 +85,13 @@ mod tests { // Now test clearing them via Some(empty vec) let clear_allows = Some(Vec::new()); - FederationAllowList::replace(pool, clear_allows) - .await - .unwrap(); - let allows = Instance::allowlist(pool).await.unwrap(); + FederationAllowList::replace(pool, clear_allows).await?; + let allows = Instance::allowlist(pool).await?; assert_eq!(0, allows.len()); - Instance::delete_all(pool).await.unwrap(); + Instance::delete_all(pool).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/impls/instance.rs b/crates/db_schema/src/impls/instance.rs index adde5482b..6c72b5e18 100644 --- a/crates/db_schema/src/impls/instance.rs +++ b/crates/db_schema/src/impls/instance.rs @@ -67,6 +67,11 @@ impl Instance { } } } + pub async fn read(pool: &mut DbPool<'_>, instance_id: InstanceId) -> Result { + let conn = &mut get_conn(pool).await?; + instance::table.find(instance_id).first(conn).await + } + pub async fn update( pool: &mut DbPool<'_>, instance_id: InstanceId, diff --git a/crates/db_schema/src/impls/language.rs b/crates/db_schema/src/impls/language.rs index 6a7b4e9ac..57420fcd4 100644 --- a/crates/db_schema/src/impls/language.rs +++ b/crates/db_schema/src/impls/language.rs @@ -1,3 +1,4 @@ +use super::actor_language::UNDETERMINED_ID; use crate::{ diesel::ExpressionMethods, newtypes::LanguageId, @@ -19,47 +20,42 @@ impl Language { language::table.find(id_).first(conn).await } - /// Attempts to find the given language code and return its ID. If not found, returns none. - pub async fn read_id_from_code( - pool: &mut DbPool<'_>, - code_: Option<&str>, - ) -> Result, Error> { - if let Some(code_) = code_ { - let conn = &mut get_conn(pool).await?; - Ok( - language::table - .filter(language::code.eq(code_)) - .first::(conn) - .await - .map(|l| l.id) - .ok(), - ) - } else { - Ok(None) - } + /// Attempts to find the given language code and return its ID. + pub async fn read_id_from_code(pool: &mut DbPool<'_>, code_: &str) -> Result { + let conn = &mut get_conn(pool).await?; + let res = language::table + .filter(language::code.eq(code_)) + .first::(conn) + .await + .map(|l| l.id); + + // Return undetermined by default + Ok(res.unwrap_or(UNDETERMINED_ID)) } } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{source::language::Language, utils::build_db_pool_for_tests}; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_languages() { + async fn test_languages() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let all = Language::read_all(pool).await.unwrap(); + let all = Language::read_all(pool).await?; assert_eq!(184, all.len()); assert_eq!("ak", all[5].code); assert_eq!("lv", all[99].code); assert_eq!("yi", all[179].code); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index a06f22e34..235f053c1 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -369,7 +369,6 @@ pub struct UserBackupLists { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ source::{ diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/moderator.rs index 14964aa00..b2ef26e69 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/moderator.rs @@ -66,6 +66,20 @@ impl Crud for ModRemovePost { } } +impl ModRemovePost { + pub async fn create_multiple( + pool: &mut DbPool<'_>, + forms: &Vec, + ) -> Result { + use crate::schema::mod_remove_post::dsl::mod_remove_post; + let conn = &mut get_conn(pool).await?; + insert_into(mod_remove_post) + .values(forms) + .execute(conn) + .await + } +} + #[async_trait] impl Crud for ModLockPost { type InsertForm = ModLockPostForm; @@ -153,6 +167,20 @@ impl Crud for ModRemoveComment { } } +impl ModRemoveComment { + pub async fn create_multiple( + pool: &mut DbPool<'_>, + forms: &Vec, + ) -> Result { + use crate::schema::mod_remove_comment::dsl::mod_remove_comment; + let conn = &mut get_conn(pool).await?; + insert_into(mod_remove_comment) + .values(forms) + .execute(conn) + .await + } +} + #[async_trait] impl Crud for ModRemoveCommunity { type InsertForm = ModRemoveCommunityForm; @@ -465,8 +493,6 @@ impl Crud for AdminPurgeComment { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -500,26 +526,25 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_mod = PersonInsertForm::test_form(inserted_instance.id, "the mod"); - let inserted_mod = Person::create(pool, &new_mod).await.unwrap(); + let inserted_mod = Person::create(pool, &new_mod).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "jim2"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -528,21 +553,21 @@ mod tests { "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let new_post = PostInsertForm::new( "A test post thweep".into(), inserted_person.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_person.id, inserted_post.id, "A test comment".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; // Now the actual tests @@ -553,12 +578,8 @@ mod tests { reason: None, removed: None, }; - let inserted_mod_remove_post = ModRemovePost::create(pool, &mod_remove_post_form) - .await - .unwrap(); - let read_mod_remove_post = ModRemovePost::read(pool, inserted_mod_remove_post.id) - .await - .unwrap(); + let inserted_mod_remove_post = ModRemovePost::create(pool, &mod_remove_post_form).await?; + let read_mod_remove_post = ModRemovePost::read(pool, inserted_mod_remove_post.id).await?; let expected_mod_remove_post = ModRemovePost { id: inserted_mod_remove_post.id, post_id: inserted_post.id, @@ -575,12 +596,8 @@ mod tests { post_id: inserted_post.id, locked: None, }; - let inserted_mod_lock_post = ModLockPost::create(pool, &mod_lock_post_form) - .await - .unwrap(); - let read_mod_lock_post = ModLockPost::read(pool, inserted_mod_lock_post.id) - .await - .unwrap(); + let inserted_mod_lock_post = ModLockPost::create(pool, &mod_lock_post_form).await?; + let read_mod_lock_post = ModLockPost::read(pool, inserted_mod_lock_post.id).await?; let expected_mod_lock_post = ModLockPost { id: inserted_mod_lock_post.id, post_id: inserted_post.id, @@ -597,12 +614,8 @@ mod tests { featured: false, is_featured_community: true, }; - let inserted_mod_feature_post = ModFeaturePost::create(pool, &mod_feature_post_form) - .await - .unwrap(); - let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id) - .await - .unwrap(); + let inserted_mod_feature_post = ModFeaturePost::create(pool, &mod_feature_post_form).await?; + let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id).await?; let expected_mod_feature_post = ModFeaturePost { id: inserted_mod_feature_post.id, post_id: inserted_post.id, @@ -620,12 +633,10 @@ mod tests { reason: None, removed: None, }; - let inserted_mod_remove_comment = ModRemoveComment::create(pool, &mod_remove_comment_form) - .await - .unwrap(); - let read_mod_remove_comment = ModRemoveComment::read(pool, inserted_mod_remove_comment.id) - .await - .unwrap(); + let inserted_mod_remove_comment = + ModRemoveComment::create(pool, &mod_remove_comment_form).await?; + let read_mod_remove_comment = + ModRemoveComment::read(pool, inserted_mod_remove_comment.id).await?; let expected_mod_remove_comment = ModRemoveComment { id: inserted_mod_remove_comment.id, comment_id: inserted_comment.id, @@ -644,13 +655,9 @@ mod tests { removed: None, }; let inserted_mod_remove_community = - ModRemoveCommunity::create(pool, &mod_remove_community_form) - .await - .unwrap(); + ModRemoveCommunity::create(pool, &mod_remove_community_form).await?; let read_mod_remove_community = - ModRemoveCommunity::read(pool, inserted_mod_remove_community.id) - .await - .unwrap(); + ModRemoveCommunity::read(pool, inserted_mod_remove_community.id).await?; let expected_mod_remove_community = ModRemoveCommunity { id: inserted_mod_remove_community.id, community_id: inserted_community.id, @@ -671,13 +678,9 @@ mod tests { expires: None, }; let inserted_mod_ban_from_community = - ModBanFromCommunity::create(pool, &mod_ban_from_community_form) - .await - .unwrap(); + ModBanFromCommunity::create(pool, &mod_ban_from_community_form).await?; let read_mod_ban_from_community = - ModBanFromCommunity::read(pool, inserted_mod_ban_from_community.id) - .await - .unwrap(); + ModBanFromCommunity::read(pool, inserted_mod_ban_from_community.id).await?; let expected_mod_ban_from_community = ModBanFromCommunity { id: inserted_mod_ban_from_community.id, community_id: inserted_community.id, @@ -698,8 +701,8 @@ mod tests { banned: None, expires: None, }; - let inserted_mod_ban = ModBan::create(pool, &mod_ban_form).await.unwrap(); - let read_mod_ban = ModBan::read(pool, inserted_mod_ban.id).await.unwrap(); + let inserted_mod_ban = ModBan::create(pool, &mod_ban_form).await?; + let read_mod_ban = ModBan::read(pool, inserted_mod_ban.id).await?; let expected_mod_ban = ModBan { id: inserted_mod_ban.id, mod_person_id: inserted_mod.id, @@ -718,12 +721,8 @@ mod tests { community_id: inserted_community.id, removed: None, }; - let inserted_mod_add_community = ModAddCommunity::create(pool, &mod_add_community_form) - .await - .unwrap(); - let read_mod_add_community = ModAddCommunity::read(pool, inserted_mod_add_community.id) - .await - .unwrap(); + let inserted_mod_add_community = ModAddCommunity::create(pool, &mod_add_community_form).await?; + let read_mod_add_community = ModAddCommunity::read(pool, inserted_mod_add_community.id).await?; let expected_mod_add_community = ModAddCommunity { id: inserted_mod_add_community.id, community_id: inserted_community.id, @@ -740,8 +739,8 @@ mod tests { other_person_id: inserted_person.id, removed: None, }; - let inserted_mod_add = ModAdd::create(pool, &mod_add_form).await.unwrap(); - let read_mod_add = ModAdd::read(pool, inserted_mod_add.id).await.unwrap(); + let inserted_mod_add = ModAdd::create(pool, &mod_add_form).await?; + let read_mod_add = ModAdd::read(pool, inserted_mod_add.id).await?; let expected_mod_add = ModAdd { id: inserted_mod_add.id, mod_person_id: inserted_mod.id, @@ -750,14 +749,12 @@ mod tests { when_: inserted_mod_add.when_, }; - Comment::delete(pool, inserted_comment.id).await.unwrap(); - Post::delete(pool, inserted_post.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Person::delete(pool, inserted_mod.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Comment::delete(pool, inserted_comment.id).await?; + Post::delete(pool, inserted_post.id).await?; + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_person.id).await?; + Person::delete(pool, inserted_mod.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_mod_remove_post, read_mod_remove_post); assert_eq!(expected_mod_lock_post, read_mod_lock_post); @@ -768,5 +765,7 @@ mod tests { assert_eq!(expected_mod_ban, read_mod_ban); assert_eq!(expected_mod_add_community, read_mod_add_community); assert_eq!(expected_mod_add, read_mod_add); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/oauth_account.rs b/crates/db_schema/src/impls/oauth_account.rs index 921a21d3d..7210b7a37 100644 --- a/crates/db_schema/src/impls/oauth_account.rs +++ b/crates/db_schema/src/impls/oauth_account.rs @@ -1,32 +1,13 @@ use crate::{ - newtypes::{LocalUserId, OAuthProviderId}, + newtypes::LocalUserId, schema::{oauth_account, oauth_account::dsl::local_user_id}, source::oauth_account::{OAuthAccount, OAuthAccountInsertForm}, utils::{get_conn, DbPool}, }; -use diesel::{ - dsl::{exists, insert_into}, - result::Error, - select, - ExpressionMethods, - QueryDsl, -}; +use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; impl OAuthAccount { - pub async fn read( - pool: &mut DbPool<'_>, - for_oauth_provider_id: OAuthProviderId, - for_local_user_id: LocalUserId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - select(exists( - oauth_account::table.find((for_oauth_provider_id, for_local_user_id)), - )) - .get_result(conn) - .await - } - pub async fn create(pool: &mut DbPool<'_>, form: &OAuthAccountInsertForm) -> Result { let conn = &mut get_conn(pool).await?; insert_into(oauth_account::table) @@ -35,17 +16,6 @@ impl OAuthAccount { .await } - pub async fn delete( - pool: &mut DbPool<'_>, - for_oauth_provider_id: OAuthProviderId, - for_local_user_id: LocalUserId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - diesel::delete(oauth_account::table.find((for_oauth_provider_id, for_local_user_id))) - .execute(conn) - .await - } - pub async fn delete_user_accounts( pool: &mut DbPool<'_>, for_local_user_id: LocalUserId, diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index 8ce7640a5..015db5581 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -42,8 +42,6 @@ impl PasswordResetRequest { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index 940ded14c..67865a783 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -21,6 +21,7 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[async_trait] impl Crud for Person { @@ -121,16 +122,18 @@ impl Person { .await } - pub async fn is_username_taken(pool: &mut DbPool<'_>, username: &str) -> Result { + pub async fn check_username_taken(pool: &mut DbPool<'_>, username: &str) -> LemmyResult<()> { use diesel::dsl::{exists, select}; let conn = &mut get_conn(pool).await?; - select(exists( + select(not(exists( person::table .filter(lower(person::name).eq(username.to_lowercase())) .filter(person::local.eq(true)), - )) - .get_result(conn) - .await + ))) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::UsernameAlreadyExists.into()) } } @@ -232,7 +235,6 @@ impl PersonFollower { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index 6a89dd577..fb6245585 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -145,7 +145,7 @@ impl Post { pool: &mut DbPool<'_>, for_creator_id: PersonId, for_community_id: Option, - new_removed: bool, + removed: bool, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; @@ -157,7 +157,7 @@ impl Post { } update - .set((post::removed.eq(new_removed), post::updated.eq(naive_now()))) + .set((post::removed.eq(removed), post::updated.eq(naive_now()))) .get_results::(conn) .await } @@ -258,9 +258,9 @@ impl Post { post::table .inner_join(person::table) .inner_join(community::table) - // find all posts which have scheduled_publish_time that is in the past + // find all posts which have scheduled_publish_time that is in the future .filter(post::scheduled_publish_time.is_not_null()) - .filter(coalesce(post::scheduled_publish_time, now()).lt(now())) + .filter(coalesce(post::scheduled_publish_time, now()).gt(now())) // make sure the post and community are still around .filter(not(post::deleted.or(post::removed))) .filter(not(community::removed.or(community::deleted))) @@ -392,8 +392,6 @@ impl PostHide { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -415,6 +413,8 @@ mod tests { traits::{Crud, Likeable, Saveable}, utils::build_db_pool_for_tests, }; + use chrono::DateTime; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; use std::collections::HashSet; @@ -422,17 +422,15 @@ mod tests { #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "jim"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -441,21 +439,27 @@ mod tests { "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let new_post = PostInsertForm::new( "A test post".into(), inserted_person.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let new_post2 = PostInsertForm::new( "A test post 2".into(), inserted_person.id, inserted_community.id, ); - let inserted_post2 = Post::create(pool, &new_post2).await.unwrap(); + let inserted_post2 = Post::create(pool, &new_post2).await?; + + let new_scheduled_post = PostInsertForm { + scheduled_publish_time: Some(DateTime::from_timestamp_nanos(i64::MAX)), + ..PostInsertForm::new("beans".into(), inserted_person.id, inserted_community.id) + }; + let inserted_scheduled_post = Post::create(pool, &new_scheduled_post).await?; let expected_post = Post { id: inserted_post.id, @@ -475,9 +479,7 @@ mod tests { embed_description: None, embed_video_url: None, thumbnail_url: None, - ap_id: Url::parse(&format!("https://lemmy-alpha/post/{}", inserted_post.id)) - .unwrap() - .into(), + ap_id: Url::parse(&format!("https://lemmy-alpha/post/{}", inserted_post.id))?.into(), local: true, language_id: Default::default(), featured_community: false, @@ -493,7 +495,7 @@ mod tests { score: 1, }; - let inserted_post_like = PostLike::like(pool, &post_like_form).await.unwrap(); + let inserted_post_like = PostLike::like(pool, &post_like_form).await?; let expected_post_like = PostLike { post_id: inserted_post.id, @@ -508,7 +510,7 @@ mod tests { person_id: inserted_person.id, }; - let inserted_post_saved = PostSaved::save(pool, &post_saved_form).await.unwrap(); + let inserted_post_saved = PostSaved::save(pool, &post_saved_form).await?; let expected_post_saved = PostSaved { post_id: inserted_post.id, @@ -522,48 +524,47 @@ mod tests { HashSet::from([inserted_post.id, inserted_post2.id]), inserted_person.id, ) - .await - .unwrap(); + .await?; assert_eq!(2, marked_as_read); - let read_post = Post::read(pool, inserted_post.id).await.unwrap(); + let read_post = Post::read(pool, inserted_post.id).await?; let new_post_update = PostUpdateForm { name: Some("A test post".into()), ..Default::default() }; - let updated_post = Post::update(pool, inserted_post.id, &new_post_update) - .await - .unwrap(); + let updated_post = Post::update(pool, inserted_post.id, &new_post_update).await?; - let like_removed = PostLike::remove(pool, inserted_person.id, inserted_post.id) - .await - .unwrap(); + // Scheduled post count + let scheduled_post_count = Post::user_scheduled_post_count(inserted_person.id, pool).await?; + assert_eq!(1, scheduled_post_count); + + let like_removed = PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; assert_eq!(1, like_removed); - let saved_removed = PostSaved::unsave(pool, &post_saved_form).await.unwrap(); + let saved_removed = PostSaved::unsave(pool, &post_saved_form).await?; assert_eq!(1, saved_removed); let read_removed = PostRead::mark_as_unread( pool, HashSet::from([inserted_post.id, inserted_post2.id]), inserted_person.id, ) - .await - .unwrap(); + .await?; assert_eq!(2, read_removed); - let num_deleted = Post::delete(pool, inserted_post.id).await.unwrap() - + Post::delete(pool, inserted_post2.id).await.unwrap(); - assert_eq!(2, num_deleted); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let num_deleted = Post::delete(pool, inserted_post.id).await? + + Post::delete(pool, inserted_post2.id).await? + + Post::delete(pool, inserted_scheduled_post.id).await?; + assert_eq!(3, num_deleted); + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_post, read_post); assert_eq!(expected_post, inserted_post); assert_eq!(expected_post, updated_post); assert_eq!(expected_post_like, inserted_post_like); assert_eq!(expected_post_saved, inserted_post_saved); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index ccb2f83fb..5507423e1 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -80,8 +80,6 @@ impl Reportable for PostReport { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; @@ -95,14 +93,13 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use diesel::result::Error; use serial_test::serial; - async fn init(pool: &mut DbPool<'_>) -> (Person, PostReport) { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + async fn init(pool: &mut DbPool<'_>) -> Result<(Person, PostReport), Error> { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let person_form = PersonInsertForm::test_form(inserted_instance.id, "jim"); - let person = Person::create(pool, &person_form).await.unwrap(); + let person = Person::create(pool, &person_form).await?; let community_form = CommunityInsertForm::new( inserted_instance.id, @@ -110,10 +107,10 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let community = Community::create(pool, &community_form).await.unwrap(); + let community = Community::create(pool, &community_form).await?; let form = PostInsertForm::new("A test post".into(), person.id, community.id); - let post = Post::create(pool, &form).await.unwrap(); + let post = Post::create(pool, &form).await?; let report_form = PostReportForm { post_id: post.id, @@ -121,46 +118,46 @@ mod tests { reason: "my reason".to_string(), ..Default::default() }; - let report = PostReport::report(pool, &report_form).await.unwrap(); - (person, report) + let report = PostReport::report(pool, &report_form).await?; + + Ok((person, report)) } #[tokio::test] #[serial] - async fn test_resolve_post_report() { + async fn test_resolve_post_report() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let (person, report) = init(pool).await; + let (person, report) = init(pool).await?; - let resolved_count = PostReport::resolve(pool, report.id, person.id) - .await - .unwrap(); + let resolved_count = PostReport::resolve(pool, report.id, person.id).await?; assert_eq!(resolved_count, 1); - let unresolved_count = PostReport::unresolve(pool, report.id, person.id) - .await - .unwrap(); + let unresolved_count = PostReport::unresolve(pool, report.id, person.id).await?; assert_eq!(unresolved_count, 1); - Person::delete(pool, person.id).await.unwrap(); - Post::delete(pool, report.post_id).await.unwrap(); + Person::delete(pool, person.id).await?; + Post::delete(pool, report.post_id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn test_resolve_all_post_reports() { + async fn test_resolve_all_post_reports() -> Result<(), Error> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let (person, report) = init(pool).await; + let (person, report) = init(pool).await?; - let resolved_count = PostReport::resolve_all_for_object(pool, report.post_id, person.id) - .await - .unwrap(); + let resolved_count = + PostReport::resolve_all_for_object(pool, report.post_id, person.id).await?; assert_eq!(resolved_count, 1); - Person::delete(pool, person.id).await.unwrap(); - Post::delete(pool, report.post_id).await.unwrap(); + Person::delete(pool, person.id).await?; + Post::delete(pool, report.post_id).await?; + + Ok(()) } } diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index d0ca7378e..264175fe2 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -85,8 +85,6 @@ impl PrivateMessage { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ @@ -98,27 +96,26 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; use url::Url; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let creator_form = PersonInsertForm::test_form(inserted_instance.id, "creator_pm"); - let inserted_creator = Person::create(pool, &creator_form).await.unwrap(); + let inserted_creator = Person::create(pool, &creator_form).await?; let recipient_form = PersonInsertForm::test_form(inserted_instance.id, "recipient_pm"); - let inserted_recipient = Person::create(pool, &recipient_form).await.unwrap(); + let inserted_recipient = Person::create(pool, &recipient_form).await?; let private_message_form = PrivateMessageInsertForm::new( inserted_creator.id, @@ -126,9 +123,7 @@ mod tests { "A test private message".into(), ); - let inserted_private_message = PrivateMessage::create(pool, &private_message_form) - .await - .unwrap(); + let inserted_private_message = PrivateMessage::create(pool, &private_message_form).await?; let expected_private_message = PrivateMessage { id: inserted_private_message.id, @@ -142,15 +137,12 @@ mod tests { ap_id: Url::parse(&format!( "https://lemmy-alpha/private_message/{}", inserted_private_message.id - )) - .unwrap() + ))? .into(), local: true, }; - let read_private_message = PrivateMessage::read(pool, inserted_private_message.id) - .await - .unwrap(); + let read_private_message = PrivateMessage::read(pool, inserted_private_message.id).await?; let private_message_update_form = PrivateMessageUpdateForm { content: Some("A test private message".into()), @@ -161,8 +153,7 @@ mod tests { inserted_private_message.id, &private_message_update_form, ) - .await - .unwrap(); + .await?; let deleted_private_message = PrivateMessage::update( pool, @@ -172,8 +163,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; let marked_read_private_message = PrivateMessage::update( pool, inserted_private_message.id, @@ -182,16 +172,17 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); - Person::delete(pool, inserted_creator.id).await.unwrap(); - Person::delete(pool, inserted_recipient.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + .await?; + Person::delete(pool, inserted_creator.id).await?; + Person::delete(pool, inserted_recipient.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_private_message, read_private_message); assert_eq!(expected_private_message, updated_private_message); assert_eq!(expected_private_message, inserted_private_message); assert!(deleted_private_message.deleted); assert!(marked_read_private_message.read); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/secret.rs b/crates/db_schema/src/impls/secret.rs index 1365ea838..bfff270b6 100644 --- a/crates/db_schema/src/impls/secret.rs +++ b/crates/db_schema/src/impls/secret.rs @@ -1,5 +1,4 @@ use crate::{ - diesel::OptionalExtension, schema::secret::dsl::secret, source::secret::Secret, utils::{get_conn, DbPool}, @@ -10,12 +9,12 @@ use diesel_async::RunQueryDsl; impl Secret { /// Initialize the Secrets from the DB. /// Warning: You should only call this once. - pub async fn init(pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn init(pool: &mut DbPool<'_>) -> Result { Self::read_secrets(pool).await } - async fn read_secrets(pool: &mut DbPool<'_>) -> Result, Error> { + async fn read_secrets(pool: &mut DbPool<'_>) -> Result { let conn = &mut get_conn(pool).await?; - secret.first(conn).await.optional() + secret.first(conn).await } } diff --git a/crates/db_schema/src/impls/tagline.rs b/crates/db_schema/src/impls/tagline.rs index 656d537d6..aa5841020 100644 --- a/crates/db_schema/src/impls/tagline.rs +++ b/crates/db_schema/src/impls/tagline.rs @@ -5,7 +5,7 @@ use crate::{ traits::Crud, utils::{get_conn, limit_and_offset, DbPool}, }; -use diesel::{insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl}; +use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; #[async_trait] @@ -51,14 +51,9 @@ impl Tagline { .await } - pub async fn get_random(pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn get_random(pool: &mut DbPool<'_>) -> Result { let conn = &mut get_conn(pool).await?; sql_function!(fn random() -> Text); - tagline - .order(random()) - .limit(1) - .first::(conn) - .await - .optional() + tagline.order(random()).limit(1).first::(conn).await } } diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 7c57ddf28..dbadaaf95 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -27,7 +27,6 @@ pub mod newtypes; pub mod sensitive; #[cfg(feature = "full")] #[rustfmt::skip] -#[allow(clippy::wildcard_imports)] pub mod schema; #[cfg(feature = "full")] pub mod aliases { @@ -252,6 +251,27 @@ pub enum CommunityVisibility { LocalOnly, } +#[derive( + EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash, +)] +#[cfg_attr(feature = "full", derive(DbEnum, TS))] +#[cfg_attr( + feature = "full", + ExistingTypePath = "crate::schema::sql_types::FederationModeEnum" +)] +#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] +#[cfg_attr(feature = "full", ts(export))] +/// The federation mode for an item +pub enum FederationMode { + #[default] + /// Allows all + All, + /// Allows only local + Local, + /// Disables + Disable, +} + /// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the /// vec on failure. #[macro_export] diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 58396c66a..fe1febef5 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -191,13 +191,13 @@ impl Display for DbUrl { } // the project doesn't compile with From -#[allow(clippy::from_over_into)] +#[expect(clippy::from_over_into)] impl Into for Url { fn into(self) -> DbUrl { DbUrl(Box::new(self)) } } -#[allow(clippy::from_over_into)] +#[expect(clippy::from_over_into)] impl Into for DbUrl { fn into(self) -> Url { *self.0 diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 37bfecfd9..0d9bb86cb 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -13,6 +13,10 @@ pub mod sql_types { #[diesel(postgres_type(name = "community_visibility"))] pub struct CommunityVisibility; + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "federation_mode_enum"))] + pub struct FederationModeEnum; + #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "listing_type_enum"))] pub struct ListingTypeEnum; @@ -376,12 +380,12 @@ diesel::table! { use super::sql_types::PostListingModeEnum; use super::sql_types::PostSortTypeEnum; use super::sql_types::CommentSortTypeEnum; + use super::sql_types::FederationModeEnum; local_site (id) { id -> Int4, site_id -> Int4, site_setup -> Bool, - enable_downvotes -> Bool, community_creation_admin_only -> Bool, require_email_verification -> Bool, application_question -> Nullable, @@ -406,6 +410,10 @@ diesel::table! { default_post_sort_type -> PostSortTypeEnum, default_comment_sort_type -> CommentSortTypeEnum, oauth_registration -> Bool, + post_upvotes -> FederationModeEnum, + post_downvotes -> FederationModeEnum, + comment_upvotes -> FederationModeEnum, + comment_downvotes -> FederationModeEnum, } } @@ -467,7 +475,6 @@ diesel::table! { totp_2fa_secret -> Nullable, open_links_in_new_tab -> Bool, blur_nsfw -> Bool, - auto_expand -> Bool, infinite_scroll_enabled -> Bool, admin -> Bool, post_listing_mode -> PostListingModeEnum, diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index 5131ce7ac..5fa57fe3b 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -3,6 +3,7 @@ use crate::schema::local_site; use crate::{ newtypes::{LocalSiteId, SiteId}, CommentSortType, + FederationMode, ListingType, PostListingMode, PostSortType, @@ -27,8 +28,6 @@ pub struct LocalSite { pub site_id: SiteId, /// True if the site is set up. pub site_setup: bool, - /// Whether downvotes are enabled. - pub enable_downvotes: bool, /// Whether only admins can create communities. pub community_creation_admin_only: bool, /// Whether emails are required. @@ -72,6 +71,14 @@ pub struct LocalSite { pub default_comment_sort_type: CommentSortType, /// Whether or not external auth methods can auto-register users. pub oauth_registration: bool, + /// What kind of post upvotes your site allows. + pub post_upvotes: FederationMode, + /// What kind of post downvotes your site allows. + pub post_downvotes: FederationMode, + /// What kind of comment upvotes your site allows. + pub comment_upvotes: FederationMode, + /// What kind of comment downvotes your site allows. + pub comment_downvotes: FederationMode, } #[derive(Clone, derive_new::new)] @@ -82,8 +89,6 @@ pub struct LocalSiteInsertForm { #[new(default)] pub site_setup: Option, #[new(default)] - pub enable_downvotes: Option, - #[new(default)] pub community_creation_admin_only: Option, #[new(default)] pub require_email_verification: Option, @@ -114,8 +119,6 @@ pub struct LocalSiteInsertForm { #[new(default)] pub registration_mode: Option, #[new(default)] - pub oauth_registration: Option, - #[new(default)] pub reports_email_admins: Option, #[new(default)] pub federation_signed_fetch: Option, @@ -125,6 +128,16 @@ pub struct LocalSiteInsertForm { pub default_post_sort_type: Option, #[new(default)] pub default_comment_sort_type: Option, + #[new(default)] + pub oauth_registration: Option, + #[new(default)] + pub post_upvotes: Option, + #[new(default)] + pub post_downvotes: Option, + #[new(default)] + pub comment_upvotes: Option, + #[new(default)] + pub comment_downvotes: Option, } #[derive(Clone, Default)] @@ -132,7 +145,6 @@ pub struct LocalSiteInsertForm { #[cfg_attr(feature = "full", diesel(table_name = local_site))] pub struct LocalSiteUpdateForm { pub site_setup: Option, - pub enable_downvotes: Option, pub community_creation_admin_only: Option, pub require_email_verification: Option, pub application_question: Option>, @@ -148,11 +160,15 @@ pub struct LocalSiteUpdateForm { pub captcha_enabled: Option, pub captcha_difficulty: Option, pub registration_mode: Option, - pub oauth_registration: Option, pub reports_email_admins: Option, pub updated: Option>>, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, pub default_post_sort_type: Option, pub default_comment_sort_type: Option, + pub oauth_registration: Option, + pub post_upvotes: Option, + pub post_downvotes: Option, + pub comment_upvotes: Option, + pub comment_downvotes: Option, } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 876bfa487..37da70908 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -14,11 +14,12 @@ use serde_with::skip_serializing_none; use ts_rs::TS; #[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = local_user))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] +#[serde(default)] /// A local user. pub struct LocalUser { pub id: LocalUserId, @@ -49,7 +50,6 @@ pub struct LocalUser { /// Open links in a new tab. pub open_links_in_new_tab: bool, pub blur_nsfw: bool, - pub auto_expand: bool, /// Whether infinite scroll is enabled. pub infinite_scroll_enabled: bool, /// Whether the person is an admin. @@ -104,8 +104,6 @@ pub struct LocalUserInsertForm { #[new(default)] pub blur_nsfw: Option, #[new(default)] - pub auto_expand: Option, - #[new(default)] pub infinite_scroll_enabled: Option, #[new(default)] pub admin: Option, @@ -143,7 +141,6 @@ pub struct LocalUserUpdateForm { pub totp_2fa_secret: Option>, pub open_links_in_new_tab: Option, pub blur_nsfw: Option, - pub auto_expand: Option, pub infinite_scroll_enabled: Option, pub admin: Option, pub post_listing_mode: Option, diff --git a/crates/db_schema/src/source/oauth_provider.rs b/crates/db_schema/src/source/oauth_provider.rs index 40046c83c..75b989805 100644 --- a/crates/db_schema/src/source/oauth_provider.rs +++ b/crates/db_schema/src/source/oauth_provider.rs @@ -87,39 +87,30 @@ impl Serialize for PublicOAuthProvider { } #[derive(Debug, Clone)] -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] -#[cfg_attr(feature = "full", ts(export))] pub struct OAuthProviderInsertForm { pub display_name: String, - #[cfg_attr(feature = "full", ts(type = "string"))] pub issuer: DbUrl, - #[cfg_attr(feature = "full", ts(type = "string"))] pub authorization_endpoint: DbUrl, - #[cfg_attr(feature = "full", ts(type = "string"))] pub token_endpoint: DbUrl, - #[cfg_attr(feature = "full", ts(type = "string"))] pub userinfo_endpoint: DbUrl, pub id_claim: String, pub client_id: String, pub client_secret: String, pub scopes: String, - pub auto_verify_email: bool, - pub account_linking_enabled: bool, - pub enabled: bool, + pub auto_verify_email: Option, + pub account_linking_enabled: Option, + pub enabled: Option, } #[derive(Debug, Clone)] -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] -#[cfg_attr(feature = "full", ts(export))] pub struct OAuthProviderUpdateForm { pub display_name: Option, - #[cfg_attr(feature = "full", ts(type = "string"))] pub authorization_endpoint: Option, - #[cfg_attr(feature = "full", ts(type = "string"))] pub token_endpoint: Option, - #[cfg_attr(feature = "full", ts(type = "string"))] pub userinfo_endpoint: Option, pub id_claim: Option, pub client_secret: Option, diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index fb9c542df..1e56563bc 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -595,7 +595,6 @@ impl Queries { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 42b5c3238..753304f70 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -259,8 +259,7 @@ impl CommentReportQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{ @@ -284,27 +283,24 @@ mod tests { CommunityVisibility, SubscribedType, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_crv"); - let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); + let inserted_timmy = Person::create(pool, &new_person).await?; let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) - .await - .unwrap(); + let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; let timmy_view = LocalUserView { local_user: timmy_local_user, local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), @@ -314,12 +310,12 @@ mod tests { let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_crv"); - let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); + let inserted_sara = Person::create(pool, &new_person_2).await?; // Add a third person, since new ppl can only report something once. let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_crv"); - let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap(); + let inserted_jessica = Person::create(pool, &new_person_3).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -327,7 +323,7 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; // Make timmy a mod let timmy_moderator_form = CommunityModeratorForm { @@ -335,9 +331,7 @@ mod tests { person_id: inserted_timmy.id, }; - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form) - .await - .unwrap(); + let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; let new_post = PostInsertForm::new( "A test post crv".into(), @@ -345,14 +339,14 @@ mod tests { inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_timmy.id, inserted_post.id, "A test comment 32".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; // sara reports let sara_report_form = CommentReportForm { @@ -362,9 +356,7 @@ mod tests { reason: "from sara".into(), }; - let inserted_sara_report = CommentReport::report(pool, &sara_report_form) - .await - .unwrap(); + let inserted_sara_report = CommentReport::report(pool, &sara_report_form).await?; // jessica reports let jessica_report_form = CommentReportForm { @@ -374,18 +366,12 @@ mod tests { reason: "from jessica".into(), }; - let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form) - .await - .unwrap(); + let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; - let agg = CommentAggregates::read(pool, inserted_comment.id) - .await - .unwrap(); + let agg = CommentAggregates::read(pool, inserted_comment.id).await?; let read_jessica_report_view = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap(); + CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; let expected_jessica_report_view = CommentReportView { comment_report: inserted_jessica_report.clone(), comment: inserted_comment.clone(), @@ -514,8 +500,7 @@ mod tests { // Do a batch read of timmys reports let reports = CommentReportQuery::default() .list(pool, &timmy_view) - .await - .unwrap(); + .await?; assert_eq!( reports, @@ -526,19 +511,14 @@ mod tests { ); // Make sure the counts are correct - let report_count = CommentReportView::get_report_count(pool, inserted_timmy.id, false, None) - .await - .unwrap(); + let report_count = + CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; assert_eq!(2, report_count); // Try to resolve the report - CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap(); + CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id).await?; let read_jessica_report_view_after_resolve = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap(); + CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; let mut expected_jessica_report_view_after_resolve = expected_jessica_report_view; expected_jessica_report_view_after_resolve @@ -588,24 +568,21 @@ mod tests { ..Default::default() } .list(pool, &timmy_view) - .await - .unwrap(); + .await?; assert_eq!(reports_after_resolve[0], expected_sara_report_view); assert_eq!(reports_after_resolve.len(), 1); // Make sure the counts are correct let report_count_after_resolved = - CommentReportView::get_report_count(pool, inserted_timmy.id, false, None) - .await - .unwrap(); + CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; assert_eq!(1, report_count_after_resolved); - Person::delete(pool, inserted_timmy.id).await.unwrap(); - Person::delete(pool, inserted_sara.id).await.unwrap(); - Person::delete(pool, inserted_jessica.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Person::delete(pool, inserted_timmy.id).await?; + Person::delete(pool, inserted_sara.id).await?; + Person::delete(pool, inserted_jessica.id).await?; + Community::delete(pool, inserted_community.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 7f3c853f1..dc9aceec0 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -216,32 +216,30 @@ fn queries<'a>() -> Queries< query = query.filter(post::community_id.eq(community_id)); } - if let Some(listing_type) = options.listing_type { - let is_subscribed = exists( - community_follower::table.filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ); + let is_subscribed = exists( + community_follower::table.filter( + post::community_id + .eq(community_follower::community_id) + .and(community_follower::person_id.eq(person_id_join)), + ), + ); - match listing_type { - ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ - ListingType::Local => { - query = query - .filter(community::local.eq(true)) - .filter(community::hidden.eq(false).or(is_subscribed)) - } - ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), - ListingType::ModeratorView => { - query = query.filter(exists( - community_moderator::table.filter( - post::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id_join)), - ), - )); - } + match options.listing_type.unwrap_or_default() { + ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ + ListingType::Local => { + query = query + .filter(community::local.eq(true)) + .filter(community::hidden.eq(false).or(is_subscribed)) + } + ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), + ListingType::ModeratorView => { + query = query.filter(exists( + community_moderator::table.filter( + post::community_id + .eq(community_moderator::community_id) + .and(community_moderator::person_id.eq(person_id_join)), + ), + )); } } @@ -422,8 +420,7 @@ impl<'a> CommentQuery<'a> { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] -#[allow(clippy::unwrap_used)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{ @@ -511,7 +508,7 @@ mod tests { inserted_community.id, ); let inserted_post = Post::create(pool, &new_post).await?; - let english_id = Language::read_id_from_code(pool, Some("en")).await?; + let english_id = Language::read_id_from_code(pool, "en").await?; // Create a comment tree with this hierarchy // 0 @@ -522,7 +519,7 @@ mod tests { // \ // 5 let comment_form_0 = CommentInsertForm { - language_id: english_id, + language_id: Some(english_id), ..CommentInsertForm::new( inserted_timmy_person.id, inserted_post.id, @@ -533,7 +530,7 @@ mod tests { let inserted_comment_0 = Comment::create(pool, &comment_form_0, None).await?; let comment_form_1 = CommentInsertForm { - language_id: english_id, + language_id: Some(english_id), ..CommentInsertForm::new( inserted_sara_person.id, inserted_post.id, @@ -543,9 +540,9 @@ mod tests { let inserted_comment_1 = Comment::create(pool, &comment_form_1, Some(&inserted_comment_0.path)).await?; - let finnish_id = Language::read_id_from_code(pool, Some("fi")).await?; + let finnish_id = Language::read_id_from_code(pool, "fi").await?; let comment_form_2 = CommentInsertForm { - language_id: finnish_id, + language_id: Some(finnish_id), ..CommentInsertForm::new( inserted_timmy_person.id, inserted_post.id, @@ -557,7 +554,7 @@ mod tests { Comment::create(pool, &comment_form_2, Some(&inserted_comment_0.path)).await?; let comment_form_3 = CommentInsertForm { - language_id: english_id, + language_id: Some(english_id), ..CommentInsertForm::new( inserted_timmy_person.id, inserted_post.id, @@ -567,9 +564,7 @@ mod tests { let _inserted_comment_3 = Comment::create(pool, &comment_form_3, Some(&inserted_comment_1.path)).await?; - let polish_id = Language::read_id_from_code(pool, Some("pl")) - .await? - .unwrap(); + let polish_id = Language::read_id_from_code(pool, "pl").await?; let comment_form_4 = CommentInsertForm { language_id: Some(polish_id), ..CommentInsertForm::new( @@ -655,8 +650,8 @@ mod tests { .await?; assert_eq!( - &expected_comment_view_no_person, - read_comment_views_no_person.first().unwrap() + Some(&expected_comment_view_no_person), + read_comment_views_no_person.first() ); let read_comment_views_with_person = CommentQuery { @@ -832,9 +827,7 @@ mod tests { assert_length!(5, all_languages); // change user lang to finnish, should only show one post in finnish and one undetermined - let finnish_id = Language::read_id_from_code(pool, Some("fi")) - .await? - .unwrap(); + let finnish_id = Language::read_id_from_code(pool, "fi").await?; LocalUserLanguage::update( pool, vec![finnish_id], @@ -853,8 +846,8 @@ mod tests { .find(|c| c.comment.language_id == finnish_id); assert!(finnish_comment.is_some()); assert_eq!( - data.inserted_comment_2.content, - finnish_comment.unwrap().comment.content + Some(&data.inserted_comment_2.content), + finnish_comment.map(|c| &c.comment.content) ); // now show all comments with undetermined language (which is the default value) diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index 527ebdafa..82e4c5d5b 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -284,8 +284,7 @@ impl PostReportQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{ @@ -306,27 +305,24 @@ mod tests { traits::{Crud, Joinable, Reportable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_prv"); - let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); + let inserted_timmy = Person::create(pool, &new_person).await?; let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) - .await - .unwrap(); + let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; let timmy_view = LocalUserView { local_user: timmy_local_user, local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), @@ -336,12 +332,12 @@ mod tests { let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_prv"); - let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); + let inserted_sara = Person::create(pool, &new_person_2).await?; // Add a third person, since new ppl can only report something once. let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_prv"); - let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap(); + let inserted_jessica = Person::create(pool, &new_person_3).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -349,7 +345,7 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; // Make timmy a mod let timmy_moderator_form = CommunityModeratorForm { @@ -357,16 +353,14 @@ mod tests { person_id: inserted_timmy.id, }; - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form) - .await - .unwrap(); + let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; let new_post = PostInsertForm::new( "A test post crv".into(), inserted_timmy.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; // sara reports let sara_report_form = PostReportForm { @@ -378,14 +372,14 @@ mod tests { reason: "from sara".into(), }; - PostReport::report(pool, &sara_report_form).await.unwrap(); + PostReport::report(pool, &sara_report_form).await?; let new_post_2 = PostInsertForm::new( "A test post crv 2".into(), inserted_timmy.id, inserted_community.id, ); - let inserted_post_2 = Post::create(pool, &new_post_2).await.unwrap(); + let inserted_post_2 = Post::create(pool, &new_post_2).await?; // jessica reports let jessica_report_form = PostReportForm { @@ -397,14 +391,10 @@ mod tests { reason: "from jessica".into(), }; - let inserted_jessica_report = PostReport::report(pool, &jessica_report_form) - .await - .unwrap(); + let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; let read_jessica_report_view = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap(); + PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; assert_eq!( read_jessica_report_view.post_report, @@ -418,30 +408,23 @@ mod tests { assert_eq!(read_jessica_report_view.resolver, None); // Do a batch read of timmys reports - let reports = PostReportQuery::default() - .list(pool, &timmy_view) - .await - .unwrap(); + let reports = PostReportQuery::default().list(pool, &timmy_view).await?; assert_eq!(reports[1].creator.id, inserted_sara.id); assert_eq!(reports[0].creator.id, inserted_jessica.id); // Make sure the counts are correct - let report_count = PostReportView::get_report_count(pool, inserted_timmy.id, false, None) - .await - .unwrap(); + let report_count = + PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; assert_eq!(2, report_count); // Pretend the post was removed, and resolve all reports for that object. // This is called manually in the API for post removals PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, inserted_timmy.id) - .await - .unwrap(); + .await?; let read_jessica_report_view_after_resolve = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) - .await - .unwrap(); + PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; assert!(read_jessica_report_view_after_resolve.post_report.resolved); assert_eq!( read_jessica_report_view_after_resolve @@ -450,8 +433,10 @@ mod tests { Some(inserted_timmy.id) ); assert_eq!( - read_jessica_report_view_after_resolve.resolver.unwrap().id, - inserted_timmy.id + read_jessica_report_view_after_resolve + .resolver + .map(|r| r.id), + Some(inserted_timmy.id) ); // Do a batch read of timmys reports @@ -461,24 +446,21 @@ mod tests { ..Default::default() } .list(pool, &timmy_view) - .await - .unwrap(); + .await?; assert_length!(1, reports_after_resolve); assert_eq!(reports_after_resolve[0].creator.id, inserted_sara.id); // Make sure the counts are correct let report_count_after_resolved = - PostReportView::get_report_count(pool, inserted_timmy.id, false, None) - .await - .unwrap(); + PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; assert_eq!(1, report_count_after_resolved); - Person::delete(pool, inserted_timmy.id).await.unwrap(); - Person::delete(pool, inserted_sara.id).await.unwrap(); - Person::delete(pool, inserted_jessica.id).await.unwrap(); - Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Person::delete(pool, inserted_timmy.id).await?; + Person::delete(pool, inserted_sara.id).await?; + Person::delete(pool, inserted_jessica.id).await?; + Community::delete(pool, inserted_community.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index a8d908c7d..e07133471 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -346,47 +346,30 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::creator_id.eq(creator_id)); } - if let Some(listing_type) = options.listing_type { - if let Some(person_id) = options.local_user.person_id() { - let is_subscribed = exists( - community_follower::table.filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), + let is_subscribed = exists( + community_follower::table.filter( + post_aggregates::community_id + .eq(community_follower::community_id) + .and(community_follower::person_id.eq(person_id_join)), + ), + ); + match options.listing_type.unwrap_or_default() { + ListingType::Subscribed => query = query.filter(is_subscribed), + ListingType::Local => { + query = query + .filter(community::local.eq(true)) + .filter(community::hidden.eq(false).or(is_subscribed)); + } + ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), + ListingType::ModeratorView => { + query = query.filter(exists( + community_moderator::table.filter( + post::community_id + .eq(community_moderator::community_id) + .and(community_moderator::person_id.eq(person_id_join)), ), - ); - match listing_type { - ListingType::Subscribed => query = query.filter(is_subscribed), - ListingType::Local => { - query = query - .filter(community::local.eq(true)) - .filter(community::hidden.eq(false).or(is_subscribed)); - } - ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), - ListingType::ModeratorView => { - query = query.filter(exists( - community_moderator::table.filter( - post::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id)), - ), - )); - } - } + )); } - // If your person_id is missing, only show local - else { - match listing_type { - ListingType::Local => { - query = query - .filter(community::local.eq(true)) - .filter(community::hidden.eq(false)); - } - _ => query = query.filter(community::hidden.eq(false)), - } - } - } else { - query = query.filter(community::hidden.eq(false)); } if let Some(search_term) = &options.search_term { @@ -394,14 +377,12 @@ fn queries<'a>() -> Queries< query = query.filter(post::url.eq(search_term)); } else { let searcher = fuzzy_search(search_term); + let name_filter = post::name.ilike(searcher.clone()); + let body_filter = post::body.ilike(searcher.clone()); query = if options.title_only.unwrap_or_default() { - query.filter(post::name.ilike(searcher)) + query.filter(name_filter) } else { - query.filter( - post::name - .ilike(searcher.clone()) - .or(post::body.ilike(searcher)), - ) + query.filter(name_filter.or(body_filter)) } .filter(not(post::removed.or(post::deleted))); } @@ -741,7 +722,6 @@ impl<'a> PostQuery<'a> { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use crate::{ post_view::{PaginationCursorData, PostQuery, PostView}, @@ -757,6 +737,8 @@ mod tests { comment::{Comment, CommentInsertForm}, community::{ Community, + CommunityFollower, + CommunityFollowerForm, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -785,7 +767,7 @@ mod tests { }, site::Site, }, - traits::{Bannable, Blockable, Crud, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, @@ -1307,13 +1289,9 @@ mod tests { let pool = &mut pool.into(); let data = init_data(pool).await?; - let spanish_id = Language::read_id_from_code(pool, Some("es")) - .await? - .expect("spanish should exist"); + let spanish_id = Language::read_id_from_code(pool, "es").await?; - let french_id = Language::read_id_from_code(pool, Some("fr")) - .await? - .expect("french should exist"); + let french_id = Language::read_id_from_code(pool, "fr").await?; let post_spanish = PostInsertForm { language_id: Some(spanish_id), @@ -1439,6 +1417,43 @@ mod tests { cleanup(data, pool).await } + #[tokio::test] + #[serial] + async fn post_listings_hidden_community() -> LemmyResult<()> { + let pool = &build_db_pool().await?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + Community::update( + pool, + data.inserted_community.id, + &CommunityUpdateForm { + hidden: Some(true), + ..Default::default() + }, + ) + .await?; + + let posts = PostQuery::default().list(&data.site, pool).await?; + assert!(posts.is_empty()); + + let posts = data.default_post_query().list(&data.site, pool).await?; + assert!(posts.is_empty()); + + // Follow the community + let form = CommunityFollowerForm { + community_id: data.inserted_community.id, + person_id: data.local_user_view.person.id, + pending: false, + }; + CommunityFollower::follow(pool, &form).await?; + + let posts = data.default_post_query().list(&data.site, pool).await?; + assert!(!posts.is_empty()); + + cleanup(data, pool).await + } + #[tokio::test] #[serial] async fn post_listing_instance_block() -> LemmyResult<()> { @@ -1692,7 +1707,7 @@ mod tests { assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_hidden)); // Make sure that hidden field is true. - assert!(&post_listings_show_hidden.first().unwrap().hidden); + assert!(&post_listings_show_hidden.first().is_some_and(|p| p.hidden)); cleanup(data, pool).await } @@ -1728,7 +1743,7 @@ mod tests { assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_nsfw)); // Make sure that nsfw field is true. - assert!(&post_listings_show_nsfw.first().unwrap().post.nsfw); + assert!(&post_listings_show_nsfw.first().is_some_and(|p| p.post.nsfw)); cleanup(data, pool).await } diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index 49b8e415b..56d0d6e7b 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -111,8 +111,7 @@ impl PrivateMessageReportQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::private_message_report_view::PrivateMessageReportQuery; @@ -127,24 +126,23 @@ mod tests { traits::{Crud, Reportable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person_1 = PersonInsertForm::test_form(inserted_instance.id, "timmy_mrv"); - let inserted_timmy = Person::create(pool, &new_person_1).await.unwrap(); + let inserted_timmy = Person::create(pool, &new_person_1).await?; let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); - let inserted_jessica = Person::create(pool, &new_person_2).await.unwrap(); + let inserted_jessica = Person::create(pool, &new_person_2).await?; // timmy sends private message to jessica let pm_form = PrivateMessageInsertForm::new( @@ -152,7 +150,7 @@ mod tests { inserted_jessica.id, "something offensive".to_string(), ); - let pm = PrivateMessage::create(pool, &pm_form).await.unwrap(); + let pm = PrivateMessage::create(pool, &pm_form).await?; // jessica reports private message let pm_report_form = PrivateMessageReportForm { @@ -161,14 +159,9 @@ mod tests { private_message_id: pm.id, reason: "its offensive".to_string(), }; - let pm_report = PrivateMessageReport::report(pool, &pm_report_form) - .await - .unwrap(); + let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; - let reports = PrivateMessageReportQuery::default() - .list(pool) - .await - .unwrap(); + let reports = PrivateMessageReportQuery::default().list(pool).await?; assert_length!(1, reports); assert!(!reports[0].private_message_report.resolved); assert_eq!(inserted_timmy.name, reports[0].private_message_creator.name); @@ -177,28 +170,27 @@ mod tests { assert_eq!(pm.content, reports[0].private_message.content); let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "admin_mrv"); - let inserted_admin = Person::create(pool, &new_person_3).await.unwrap(); + let inserted_admin = Person::create(pool, &new_person_3).await?; // admin resolves the report (after taking appropriate action) - PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id) - .await - .unwrap(); + PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id).await?; let reports = PrivateMessageReportQuery { unresolved_only: (false), ..Default::default() } .list(pool) - .await - .unwrap(); + .await?; assert_length!(1, reports); assert!(reports[0].private_message_report.resolved); assert!(reports[0].resolver.is_some()); assert_eq!( - inserted_admin.name, - reports[0].resolver.as_ref().unwrap().name + Some(&inserted_admin.name), + reports[0].resolver.as_ref().map(|r| &r.name) ); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index cd475897f..0fbc0ee16 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -173,8 +173,7 @@ impl PrivateMessageQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView}; @@ -205,45 +204,35 @@ mod tests { async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { let message_content = String::new(); - let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_rav"); - let timmy = Person::create(pool, &timmy_form).await.unwrap(); + let timmy = Person::create(pool, &timmy_form).await?; let sara_form = PersonInsertForm::test_form(instance.id, "sara_rav"); - let sara = Person::create(pool, &sara_form).await.unwrap(); + let sara = Person::create(pool, &sara_form).await?; let jess_form = PersonInsertForm::test_form(instance.id, "jess_rav"); - let jess = Person::create(pool, &jess_form).await.unwrap(); + let jess = Person::create(pool, &jess_form).await?; let sara_timmy_message_form = PrivateMessageInsertForm::new(sara.id, timmy.id, message_content.clone()); - PrivateMessage::create(pool, &sara_timmy_message_form) - .await - .unwrap(); + PrivateMessage::create(pool, &sara_timmy_message_form).await?; let sara_jess_message_form = PrivateMessageInsertForm::new(sara.id, jess.id, message_content.clone()); - PrivateMessage::create(pool, &sara_jess_message_form) - .await - .unwrap(); + PrivateMessage::create(pool, &sara_jess_message_form).await?; let timmy_sara_message_form = PrivateMessageInsertForm::new(timmy.id, sara.id, message_content.clone()); - PrivateMessage::create(pool, &timmy_sara_message_form) - .await - .unwrap(); + PrivateMessage::create(pool, &timmy_sara_message_form).await?; let jess_timmy_message_form = PrivateMessageInsertForm::new(jess.id, timmy.id, message_content.clone()); - PrivateMessage::create(pool, &jess_timmy_message_form) - .await - .unwrap(); + PrivateMessage::create(pool, &jess_timmy_message_form).await?; Ok(Data { instance, @@ -255,7 +244,7 @@ mod tests { async fn cleanup(instance_id: InstanceId, pool: &mut DbPool<'_>) -> LemmyResult<()> { // This also deletes all persons and private messages thanks to sql `on delete cascade` - Instance::delete(pool, instance_id).await.unwrap(); + Instance::delete(pool, instance_id).await?; Ok(()) } @@ -277,8 +266,7 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(3, &timmy_messages); assert_eq!(timmy_messages[0].creator.id, jess.id); @@ -294,8 +282,7 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(2, &timmy_unread_messages); assert_eq!(timmy_unread_messages[0].creator.id, jess.id); @@ -309,8 +296,7 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(2, &timmy_sara_messages); assert_eq!(timmy_sara_messages[0].creator.id, timmy.id); @@ -324,8 +310,7 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(1, &timmy_sara_unread_messages); assert_eq!(timmy_sara_unread_messages[0].creator.id, sara.id); @@ -352,9 +337,7 @@ mod tests { target_id: sara.id, }; - let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form) - .await - .unwrap(); + let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form).await?; let expected_block = PersonBlock { person_id: timmy.id, @@ -369,14 +352,11 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(1, &timmy_messages); - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id) - .await - .unwrap(); + let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; assert_eq!(timmy_unread_messages, 1); cleanup(instance.id, pool).await @@ -399,9 +379,7 @@ mod tests { instance_id: sara.instance_id, }; - let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form) - .await - .unwrap(); + let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form).await?; let expected_instance_block = InstanceBlock { person_id: timmy.id, @@ -416,14 +394,11 @@ mod tests { ..Default::default() } .list(pool, timmy.id) - .await - .unwrap(); + .await?; assert_length!(0, &timmy_messages); - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id) - .await - .unwrap(); + let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; assert_eq!(timmy_unread_messages, 0); cleanup(instance.id, pool).await } diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index ba44e927e..6460becb7 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -135,8 +135,6 @@ impl RegistrationApplicationQuery { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::registration_application_view::{ @@ -157,38 +155,34 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rav"); - let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap(); + let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?; let timmy_local_user_form = LocalUserInsertForm::test_form_admin(inserted_timmy_person.id); - let _inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]) - .await - .unwrap(); + let _inserted_timmy_local_user = + LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rav"); - let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap(); + let inserted_sara_person = Person::create(pool, &sara_person_form).await?; let sara_local_user_form = LocalUserInsertForm::test_form(inserted_sara_person.id); - let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]) - .await - .unwrap(); + let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]).await?; // Sara creates an application let sara_app_form = RegistrationApplicationInsertForm { @@ -196,23 +190,17 @@ mod tests { answer: "LET ME IIIIINN".to_string(), }; - let sara_app = RegistrationApplication::create(pool, &sara_app_form) - .await - .unwrap(); + let sara_app = RegistrationApplication::create(pool, &sara_app_form).await?; - let read_sara_app_view = RegistrationApplicationView::read(pool, sara_app.id) - .await - .unwrap(); + let read_sara_app_view = RegistrationApplicationView::read(pool, sara_app.id).await?; let jess_person_form = PersonInsertForm::test_form(inserted_instance.id, "jess_rav"); - let inserted_jess_person = Person::create(pool, &jess_person_form).await.unwrap(); + let inserted_jess_person = Person::create(pool, &jess_person_form).await?; let jess_local_user_form = LocalUserInsertForm::test_form(inserted_jess_person.id); - let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]) - .await - .unwrap(); + let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]).await?; // Sara creates an application let jess_app_form = RegistrationApplicationInsertForm { @@ -220,13 +208,9 @@ mod tests { answer: "LET ME IIIIINN".to_string(), }; - let jess_app = RegistrationApplication::create(pool, &jess_app_form) - .await - .unwrap(); + let jess_app = RegistrationApplication::create(pool, &jess_app_form).await?; - let read_jess_app_view = RegistrationApplicationView::read(pool, jess_app.id) - .await - .unwrap(); + let read_jess_app_view = RegistrationApplicationView::read(pool, jess_app.id).await?; let mut expected_sara_app_view = RegistrationApplicationView { registration_application: sara_app.clone(), @@ -235,7 +219,6 @@ mod tests { person_id: inserted_sara_local_user.person_id, email: inserted_sara_local_user.email, show_nsfw: inserted_sara_local_user.show_nsfw, - auto_expand: inserted_sara_local_user.auto_expand, blur_nsfw: inserted_sara_local_user.blur_nsfw, theme: inserted_sara_local_user.theme, default_post_sort_type: inserted_sara_local_user.default_post_sort_type, @@ -293,8 +276,7 @@ mod tests { ..Default::default() } .list(pool) - .await - .unwrap(); + .await?; assert_eq!( apps, @@ -302,9 +284,7 @@ mod tests { ); // Make sure the counts are correct - let unread_count = RegistrationApplicationView::get_unread_count(pool, false) - .await - .unwrap(); + let unread_count = RegistrationApplicationView::get_unread_count(pool, false).await?; assert_eq!(unread_count, 2); // Approve the application @@ -313,9 +293,7 @@ mod tests { deny_reason: None, }; - RegistrationApplication::update(pool, sara_app.id, &approve_form) - .await - .unwrap(); + RegistrationApplication::update(pool, sara_app.id, &approve_form).await?; // Update the local_user row let approve_local_user_form = LocalUserUpdateForm { @@ -323,13 +301,10 @@ mod tests { ..Default::default() }; - LocalUser::update(pool, inserted_sara_local_user.id, &approve_local_user_form) - .await - .unwrap(); + LocalUser::update(pool, inserted_sara_local_user.id, &approve_local_user_form).await?; - let read_sara_app_view_after_approve = RegistrationApplicationView::read(pool, sara_app.id) - .await - .unwrap(); + let read_sara_app_view_after_approve = + RegistrationApplicationView::read(pool, sara_app.id).await?; // Make sure the columns changed expected_sara_app_view @@ -369,28 +344,23 @@ mod tests { ..Default::default() } .list(pool) - .await - .unwrap(); + .await?; assert_eq!(apps_after_resolve, vec![read_jess_app_view]); // Make sure the counts are correct - let unread_count_after_approve = RegistrationApplicationView::get_unread_count(pool, false) - .await - .unwrap(); + let unread_count_after_approve = + RegistrationApplicationView::get_unread_count(pool, false).await?; assert_eq!(unread_count_after_approve, 1); // Make sure the not undenied_only has all the apps - let all_apps = RegistrationApplicationQuery::default() - .list(pool) - .await - .unwrap(); + let all_apps = RegistrationApplicationQuery::default().list(pool).await?; assert_eq!(all_apps.len(), 2); - Person::delete(pool, inserted_timmy_person.id) - .await - .unwrap(); - Person::delete(pool, inserted_sara_person.id).await.unwrap(); - Person::delete(pool, inserted_jess_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Person::delete(pool, inserted_timmy_person.id).await?; + Person::delete(pool, inserted_sara_person.id).await?; + Person::delete(pool, inserted_jess_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views/src/vote_view.rs b/crates/db_views/src/vote_view.rs index c97f60e28..9c5fef855 100644 --- a/crates/db_views/src/vote_view.rs +++ b/crates/db_views/src/vote_view.rs @@ -83,8 +83,6 @@ impl VoteView { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::structs::VoteView; @@ -99,26 +97,25 @@ mod tests { traits::{Bannable, Crud, Likeable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn post_and_comment_vote_views() { + async fn post_and_comment_vote_views() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_vv"); - let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); + let inserted_timmy = Person::create(pool, &new_person).await?; let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_vv"); - let inserted_sara = Person::create(pool, &new_person_2).await.unwrap(); + let inserted_sara = Person::create(pool, &new_person_2).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, @@ -126,21 +123,21 @@ mod tests { "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let new_post = PostInsertForm::new( "A test post vv".into(), inserted_timmy.id, inserted_community.id, ); - let inserted_post = Post::create(pool, &new_post).await.unwrap(); + let inserted_post = Post::create(pool, &new_post).await?; let comment_form = CommentInsertForm::new( inserted_timmy.id, inserted_post.id, "A test comment vv".into(), ); - let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap(); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; // Timmy upvotes his own post let timmy_post_vote_form = PostLikeForm { @@ -148,7 +145,7 @@ mod tests { person_id: inserted_timmy.id, score: 1, }; - PostLike::like(pool, &timmy_post_vote_form).await.unwrap(); + PostLike::like(pool, &timmy_post_vote_form).await?; // Sara downvotes timmy's post let sara_post_vote_form = PostLikeForm { @@ -156,7 +153,7 @@ mod tests { person_id: inserted_sara.id, score: -1, }; - PostLike::like(pool, &sara_post_vote_form).await.unwrap(); + PostLike::like(pool, &sara_post_vote_form).await?; let expected_post_vote_views = [ VoteView { @@ -171,9 +168,7 @@ mod tests { }, ]; - let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None) - .await - .unwrap(); + let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None).await?; assert_eq!(read_post_vote_views, expected_post_vote_views); // Timothy votes down his own comment @@ -183,9 +178,7 @@ mod tests { person_id: inserted_timmy.id, score: -1, }; - CommentLike::like(pool, &timmy_comment_vote_form) - .await - .unwrap(); + CommentLike::like(pool, &timmy_comment_vote_form).await?; // Sara upvotes timmy's comment let sara_comment_vote_form = CommentLikeForm { @@ -194,9 +187,7 @@ mod tests { person_id: inserted_sara.id, score: 1, }; - CommentLike::like(pool, &sara_comment_vote_form) - .await - .unwrap(); + CommentLike::like(pool, &sara_comment_vote_form).await?; let expected_comment_vote_views = [ VoteView { @@ -211,9 +202,8 @@ mod tests { }, ]; - let read_comment_vote_views = VoteView::list_for_comment(pool, inserted_comment.id, None, None) - .await - .unwrap(); + let read_comment_vote_views = + VoteView::list_for_comment(pool, inserted_comment.id, None, None).await?; assert_eq!(read_comment_vote_views, expected_comment_vote_views); // Ban timmy from that community @@ -222,36 +212,26 @@ mod tests { person_id: inserted_timmy.id, expires: None, }; - CommunityPersonBan::ban(pool, &ban_timmy_form) - .await - .unwrap(); + CommunityPersonBan::ban(pool, &ban_timmy_form).await?; // Make sure creator_banned_from_community is true let read_comment_vote_views_after_ban = - VoteView::list_for_comment(pool, inserted_comment.id, None, None) - .await - .unwrap(); + VoteView::list_for_comment(pool, inserted_comment.id, None, None).await?; - assert!( - read_comment_vote_views_after_ban - .first() - .unwrap() - .creator_banned_from_community - ); + assert!(read_comment_vote_views_after_ban + .first() + .is_some_and(|c| c.creator_banned_from_community)); let read_post_vote_views_after_ban = - VoteView::list_for_post(pool, inserted_post.id, None, None) - .await - .unwrap(); + VoteView::list_for_post(pool, inserted_post.id, None, None).await?; - assert!( - read_post_vote_views_after_ban - .get(1) - .unwrap() - .creator_banned_from_community - ); + assert!(read_post_vote_views_after_ban + .get(1) + .is_some_and(|p| p.creator_banned_from_community)); // Cleanup - Instance::delete(pool, inserted_instance.id).await.unwrap(); + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) } } diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 6f8810986..1b657866a 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -303,7 +303,6 @@ impl CommentReplyQuery { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{comment_reply_view::CommentReplyQuery, structs::CommentReplyView}; diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index a1b387bea..9ff6fadce 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -1,4 +1,4 @@ -use crate::structs::{CommunityModeratorView, CommunityView, PersonView}; +use crate::structs::{CommunityModeratorView, CommunitySortType, CommunityView, PersonView}; use diesel::{ pg::Pg, result::Error, @@ -22,7 +22,16 @@ use lemmy_db_schema::{ instance_block, }, source::{community::CommunityFollower, local_user::LocalUser, site::Site}, - utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{ + functions::lower, + fuzzy_search, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, ListingType, PostSortType, }; @@ -103,7 +112,7 @@ fn queries<'a>() -> Queries< }; let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { - use PostSortType::*; + use CommunitySortType::*; // The left join below will return None in this case let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); @@ -112,9 +121,14 @@ fn queries<'a>() -> Queries< if let Some(search_term) = options.search_term { let searcher = fuzzy_search(&search_term); - query = query - .filter(community::name.ilike(searcher.clone())) - .or_filter(community::title.ilike(searcher)) + let name_filter = community::name.ilike(searcher.clone()); + let title_filter = community::title.ilike(searcher.clone()); + let description_filter = community::description.ilike(searcher.clone()); + query = if options.title_only.unwrap_or_default() { + query.filter(name_filter.or(title_filter)) + } else { + query.filter(name_filter.or(title_filter.or(description_filter))) + } } // Hide deleted and removed for non-admins or mods @@ -143,6 +157,8 @@ fn queries<'a>() -> Queries< } TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()), TopWeek => query = query.order_by(community_aggregates::users_active_week.desc()), + NameAsc => query = query.order_by(lower(community::name).asc()), + NameDesc => query = query.order_by(lower(community::name).desc()), }; if let Some(listing_type) = options.listing_type { @@ -223,12 +239,39 @@ impl CommunityView { } } +impl From for CommunitySortType { + fn from(value: PostSortType) -> Self { + match value { + PostSortType::Active => Self::Active, + PostSortType::Hot => Self::Hot, + PostSortType::New => Self::New, + PostSortType::Old => Self::Old, + PostSortType::TopDay => Self::TopDay, + PostSortType::TopWeek => Self::TopWeek, + PostSortType::TopMonth => Self::TopMonth, + PostSortType::TopYear => Self::TopYear, + PostSortType::TopAll => Self::TopAll, + PostSortType::MostComments => Self::MostComments, + PostSortType::NewComments => Self::NewComments, + PostSortType::TopHour => Self::TopHour, + PostSortType::TopSixHour => Self::TopSixHour, + PostSortType::TopTwelveHour => Self::TopTwelveHour, + PostSortType::TopThreeMonths => Self::TopThreeMonths, + PostSortType::TopSixMonths => Self::TopSixMonths, + PostSortType::TopNineMonths => Self::TopNineMonths, + PostSortType::Controversial => Self::Controversial, + PostSortType::Scaled => Self::Scaled, + } + } +} + #[derive(Default)] pub struct CommunityQuery<'a> { pub listing_type: Option, - pub sort: Option, + pub sort: Option, pub local_user: Option<&'a LocalUser>, pub search_term: Option, + pub title_only: Option, pub is_mod_or_admin: bool, pub show_nsfw: bool, pub page: Option, @@ -242,11 +285,12 @@ impl<'a> CommunityQuery<'a> { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { - use crate::{community_view::CommunityQuery, structs::CommunityView}; + use crate::{ + community_view::CommunityQuery, + structs::{CommunitySortType, CommunityView}, + }; use lemmy_db_schema::{ source::{ community::{Community, CommunityInsertForm, CommunityUpdateForm}, @@ -259,41 +303,63 @@ mod tests { utils::{build_db_pool_for_tests, DbPool}, CommunityVisibility, }; + use lemmy_utils::error::LemmyResult; use serial_test::serial; use url::Url; struct Data { inserted_instance: Instance, local_user: LocalUser, - inserted_community: Community, + inserted_communities: [Community; 3], site: Site, } - async fn init_data(pool: &mut DbPool<'_>) -> Data { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let person_name = "tegan".to_string(); let new_person = PersonInsertForm::test_form(inserted_instance.id, &person_name); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); - let local_user = LocalUser::create(pool, &local_user_form, vec![]) - .await - .unwrap(); + let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test_community_3".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_communities = [ + Community::create( + pool, + &CommunityInsertForm::new( + inserted_instance.id, + "test_community_1".to_string(), + "nada1".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + Community::create( + pool, + &CommunityInsertForm::new( + inserted_instance.id, + "test_community_2".to_string(), + "nada2".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + Community::create( + pool, + &CommunityInsertForm::new( + inserted_instance.id, + "test_community_3".to_string(), + "nada3".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + ]; - let url = Url::parse("http://example.com").unwrap(); + let url = Url::parse("http://example.com")?; let site = Site { id: Default::default(), name: String::new(), @@ -312,74 +378,102 @@ mod tests { content_warning: None, }; - Data { + Ok(Data { inserted_instance, local_user, - inserted_community, + inserted_communities, site, - } + }) } - async fn cleanup(data: Data, pool: &mut DbPool<'_>) { - Community::delete(pool, data.inserted_community.id) - .await - .unwrap(); - Person::delete(pool, data.local_user.person_id) - .await - .unwrap(); - Instance::delete(pool, data.inserted_instance.id) - .await - .unwrap(); + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + for Community { id, .. } in data.inserted_communities { + Community::delete(pool, id).await?; + } + Person::delete(pool, data.local_user.person_id).await?; + Instance::delete(pool, data.inserted_instance.id).await?; + + Ok(()) } #[tokio::test] #[serial] - async fn local_only_community() { + async fn local_only_community() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let data = init_data(pool).await; + let data = init_data(pool).await?; Community::update( pool, - data.inserted_community.id, + data.inserted_communities[0].id, &CommunityUpdateForm { visibility: Some(CommunityVisibility::LocalOnly), ..Default::default() }, ) - .await - .unwrap(); + .await?; let unauthenticated_query = CommunityQuery { ..Default::default() } .list(&data.site, pool) - .await - .unwrap(); - assert_eq!(0, unauthenticated_query.len()); + .await?; + assert_eq!( + data.inserted_communities.len() - 1, + unauthenticated_query.len() + ); let authenticated_query = CommunityQuery { local_user: Some(&data.local_user), ..Default::default() } .list(&data.site, pool) - .await - .unwrap(); - assert_eq!(1, authenticated_query.len()); + .await?; + assert_eq!(data.inserted_communities.len(), authenticated_query.len()); let unauthenticated_community = - CommunityView::read(pool, data.inserted_community.id, None, false).await; + CommunityView::read(pool, data.inserted_communities[0].id, None, false).await; assert!(unauthenticated_community.is_err()); let authenticated_community = CommunityView::read( pool, - data.inserted_community.id, + data.inserted_communities[0].id, Some(&data.local_user), false, ) .await; assert!(authenticated_community.is_ok()); - cleanup(data, pool).await; + cleanup(data, pool).await + } + + #[tokio::test] + #[serial] + async fn community_sort_name() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + let query = CommunityQuery { + sort: Some(CommunitySortType::NameAsc), + ..Default::default() + }; + let communities = query.list(&data.site, pool).await?; + for (i, c) in communities.iter().enumerate().skip(1) { + let prev = communities.get(i - 1).expect("No previous community?"); + assert!(c.community.title.cmp(&prev.community.title).is_ge()); + } + + let query = CommunityQuery { + sort: Some(CommunitySortType::NameDesc), + ..Default::default() + }; + let communities = query.list(&data.site, pool).await?; + for (i, c) in communities.iter().enumerate().skip(1) { + let prev = communities.get(i - 1).expect("No previous community?"); + assert!(c.community.title.cmp(&prev.community.title).is_le()); + } + + cleanup(data, pool).await } } diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index 102f95c87..2478c0183 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -303,7 +303,6 @@ impl PersonMentionQuery { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{person_mention_view::PersonMentionQuery, structs::PersonMentionView}; diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index ff4a595ff..724a700ad 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -164,7 +164,7 @@ impl PersonQuery { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use super::*; diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index 2992d575d..ecf9ba11d 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -59,6 +59,35 @@ pub struct CommunityView { pub banned_from_community: bool, } +/// The community sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub enum CommunitySortType { + #[default] + Active, + Hot, + New, + Old, + TopDay, + TopWeek, + TopMonth, + TopYear, + TopAll, + MostComments, + NewComments, + TopHour, + TopSixHour, + TopTwelveHour, + TopThreeMonths, + TopSixMonths, + TopNineMonths, + Controversial, + Scaled, + NameAsc, + NameDesc, +} + #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] diff --git a/crates/federate/src/inboxes.rs b/crates/federate/src/inboxes.rs index cda4da39b..1649e019f 100644 --- a/crates/federate/src/inboxes.rs +++ b/crates/federate/src/inboxes.rs @@ -222,14 +222,14 @@ impl CommunityInboxCollector { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod tests { use super::*; use lemmy_db_schema::{ newtypes::{ActivityId, CommunityId, InstanceId, SiteId}, source::activity::{ActorType, SentActivity}, }; + use lemmy_utils::error::LemmyResult; use mockall::{mock, predicate::*}; use serde_json::json; mock! { @@ -253,13 +253,11 @@ mod tests { } #[tokio::test] - async fn test_get_inbox_urls_empty() { + async fn test_get_inbox_urls_empty() -> LemmyResult<()> { let mut collector = setup_collector(); let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -270,14 +268,16 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert!(result.is_empty()); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_send_all_instances() { + async fn test_get_inbox_urls_send_all_instances() -> LemmyResult<()> { let mut collector = setup_collector(); - let site_inbox = Url::parse("https://example.com/inbox").unwrap(); + let site_inbox = Url::parse("https://example.com/inbox")?; let site = Site { id: SiteId(1), name: "Test Site".to_string(), @@ -287,7 +287,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site").unwrap().into(), + actor_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, @@ -303,9 +303,7 @@ mod tests { let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -316,13 +314,15 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 1); assert_eq!(result[0], site_inbox); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_community_followers() { + async fn test_get_inbox_urls_community_followers() -> LemmyResult<()> { let mut collector = setup_collector(); let community_id = CommunityId(1); let url1 = "https://follower1.example.com/inbox"; @@ -333,18 +333,22 @@ mod tests { .expect_get_instance_followed_community_inboxes() .return_once(move |_, _| { Ok(vec![ - (community_id, Url::parse(url1).unwrap().into()), - (community_id, Url::parse(url2).unwrap().into()), + ( + community_id, + Url::parse(url1).map_err(|_| diesel::NotFound)?.into(), + ), + ( + community_id, + Url::parse(url2).map_err(|_| diesel::NotFound)?.into(), + ), ]) }); - collector.update_communities().await.unwrap(); + collector.update_communities().await?; let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -355,24 +359,24 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 2); - assert!(result.contains(&Url::parse(url1).unwrap())); - assert!(result.contains(&Url::parse(url2).unwrap())); + assert!(result.contains(&Url::parse(url1)?)); + assert!(result.contains(&Url::parse(url2)?)); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_send_inboxes() { + async fn test_get_inbox_urls_send_inboxes() -> LemmyResult<()> { let mut collector = setup_collector(); collector.domain = "example.com".to_string(); - let inbox_user_1 = Url::parse("https://example.com/user1/inbox").unwrap(); - let inbox_user_2 = Url::parse("https://example.com/user2/inbox").unwrap(); - let other_domain_inbox = Url::parse("https://other-domain.com/user3/inbox").unwrap(); + let inbox_user_1 = Url::parse("https://example.com/user1/inbox")?; + let inbox_user_2 = Url::parse("https://example.com/user2/inbox")?; + let other_domain_inbox = Url::parse("https://other-domain.com/user3/inbox")?; let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -387,20 +391,22 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 2); assert!(result.contains(&inbox_user_1)); assert!(result.contains(&inbox_user_2)); assert!(!result.contains(&other_domain_inbox)); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_combined() { + async fn test_get_inbox_urls_combined() -> LemmyResult<()> { let mut collector = setup_collector(); collector.domain = "example.com".to_string(); let community_id = CommunityId(1); - let site_inbox = Url::parse("https://example.com/site_inbox").unwrap(); + let site_inbox = Url::parse("https://example.com/site_inbox")?; let site = Site { id: SiteId(1), name: "Test Site".to_string(), @@ -410,7 +416,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site").unwrap().into(), + actor_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, @@ -431,18 +437,18 @@ mod tests { .return_once(move |_, _| { Ok(vec![( community_id, - Url::parse(subdomain_inbox).unwrap().into(), + Url::parse(subdomain_inbox) + .map_err(|_| diesel::NotFound)? + .into(), )]) }); - collector.update_communities().await.unwrap(); - let user1_inbox = Url::parse("https://example.com/user1/inbox").unwrap(); - let user2_inbox = Url::parse("https://other-domain.com/user2/inbox").unwrap(); + collector.update_communities().await?; + let user1_inbox = Url::parse("https://example.com/user1/inbox")?; + let user2_inbox = Url::parse("https://other-domain.com/user2/inbox")?; let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -456,27 +462,29 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 3); assert!(result.contains(&site_inbox)); - assert!(result.contains(&Url::parse(subdomain_inbox).unwrap())); + assert!(result.contains(&Url::parse(subdomain_inbox)?)); assert!(result.contains(&user1_inbox)); assert!(!result.contains(&user2_inbox)); + + Ok(()) } #[tokio::test] - async fn test_update_communities() { + async fn test_update_communities() -> LemmyResult<()> { let mut collector = setup_collector(); let community_id1 = CommunityId(1); let community_id2 = CommunityId(2); let community_id3 = CommunityId(3); let user1_inbox_str = "https://follower1.example.com/inbox"; - let user1_inbox = Url::parse(user1_inbox_str).unwrap(); + let user1_inbox = Url::parse(user1_inbox_str)?; let user2_inbox_str = "https://follower2.example.com/inbox"; - let user2_inbox = Url::parse(user2_inbox_str).unwrap(); + let user2_inbox = Url::parse(user2_inbox_str)?; let user3_inbox_str = "https://follower3.example.com/inbox"; - let user3_inbox = Url::parse(user3_inbox_str).unwrap(); + let user3_inbox = Url::parse(user3_inbox_str)?; collector .data_source @@ -485,42 +493,57 @@ mod tests { .returning(move |_, last_fetch| { if last_fetch == Utc.timestamp_nanos(0) { Ok(vec![ - (community_id1, Url::parse(user1_inbox_str).unwrap().into()), - (community_id2, Url::parse(user2_inbox_str).unwrap().into()), + ( + community_id1, + Url::parse(user1_inbox_str) + .map_err(|_| diesel::NotFound)? + .into(), + ), + ( + community_id2, + Url::parse(user2_inbox_str) + .map_err(|_| diesel::NotFound)? + .into(), + ), ]) } else { Ok(vec![( community_id3, - Url::parse(user3_inbox_str).unwrap().into(), + Url::parse(user3_inbox_str) + .map_err(|_| diesel::NotFound)? + .into(), )]) } }); // First update - collector.update_communities().await.unwrap(); + collector.update_communities().await?; assert_eq!(collector.followed_communities.len(), 2); assert!(collector.followed_communities[&community_id1].contains(&user1_inbox)); assert!(collector.followed_communities[&community_id2].contains(&user2_inbox)); // Simulate time passing - collector.last_full_communities_fetch = Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap(); + collector.last_full_communities_fetch = + Utc::now() - chrono::TimeDelta::try_minutes(3).expect("TimeDelta out of bounds"); collector.last_incremental_communities_fetch = - Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap(); + Utc::now() - chrono::TimeDelta::try_minutes(3).expect("TimeDelta out of bounds"); // Second update (incremental) - collector.update_communities().await.unwrap(); + collector.update_communities().await?; assert_eq!(collector.followed_communities.len(), 3); assert!(collector.followed_communities[&community_id1].contains(&user1_inbox)); assert!(collector.followed_communities[&community_id3].contains(&user3_inbox)); assert!(collector.followed_communities[&community_id2].contains(&user2_inbox)); + + Ok(()) } #[tokio::test] - async fn test_get_inbox_urls_no_duplicates() { + async fn test_get_inbox_urls_no_duplicates() -> LemmyResult<()> { let mut collector = setup_collector(); collector.domain = "example.com".to_string(); let community_id = CommunityId(1); - let site_inbox = Url::parse("https://example.com/site_inbox").unwrap(); + let site_inbox = Url::parse("https://example.com/site_inbox")?; let site_inbox_clone = site_inbox.clone(); let site = Site { id: SiteId(1), @@ -531,7 +554,7 @@ mod tests { icon: None, banner: None, description: None, - actor_id: Url::parse("https://example.com/site").unwrap().into(), + actor_id: Url::parse("https://example.com/site")?.into(), last_refreshed_at: Utc::now(), inbox_url: site_inbox.clone().into(), private_key: None, @@ -550,13 +573,11 @@ mod tests { .expect_get_instance_followed_community_inboxes() .return_once(move |_, _| Ok(vec![(community_id, site_inbox_clone.into())])); - collector.update_communities().await.unwrap(); + collector.update_communities().await?; let activity = SentActivity { id: ActivityId(1), - ap_id: Url::parse("https://example.com/activities/1") - .unwrap() - .into(), + ap_id: Url::parse("https://example.com/activities/1")?.into(), data: json!({}), sensitive: false, published: Utc::now(), @@ -567,8 +588,10 @@ mod tests { actor_apub_id: None, }; - let result = collector.get_inbox_urls(&activity).await.unwrap(); + let result = collector.get_inbox_urls(&activity).await?; assert_eq!(result.len(), 1); - assert!(result.contains(&Url::parse("https://example.com/site_inbox").unwrap())); + assert!(result.contains(&Url::parse("https://example.com/site_inbox")?)); + + Ok(()) } } diff --git a/crates/federate/src/lib.rs b/crates/federate/src/lib.rs index 0e43ef018..983749de3 100644 --- a/crates/federate/src/lib.rs +++ b/crates/federate/src/lib.rs @@ -192,8 +192,8 @@ impl SendManager { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::unwrap_used)] +#[expect(clippy::indexing_slicing)] mod test { use super::*; diff --git a/crates/federate/src/worker.rs b/crates/federate/src/worker.rs index 56f42bb30..0d4ad04d5 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -439,8 +439,8 @@ impl InstanceWorker { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::unwrap_used)] +#[expect(clippy::indexing_slicing)] mod test { use super::*; diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index e03ff2e23..b0add40e7 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -46,7 +46,7 @@ pub enum LemmyErrorType { PersonIsBlocked, CommunityIsBlocked, InstanceIsBlocked, - DownvotesAreDisabled, + VoteNotAllowed, InstanceIsPrivate, /// Password must be between 10 and 60 characters InvalidPassword, @@ -288,7 +288,6 @@ cfg_if! { #[cfg(test)] mod tests { - #![allow(clippy::unwrap_used)] #![allow(clippy::indexing_slicing)] use super::*; use actix_web::{body::MessageBody, ResponseError}; @@ -297,21 +296,25 @@ cfg_if! { use strum::IntoEnumIterator; #[test] - fn deserializes_no_message() { + fn deserializes_no_message() -> LemmyResult<()> { let err = LemmyError::from(LemmyErrorType::Banned).error_response(); - let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); - assert_eq!(&json, "{\"error\":\"banned\"}") + let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?; + assert_eq!(&json, "{\"error\":\"banned\"}"); + + Ok(()) } #[test] - fn deserializes_with_message() { + fn deserializes_with_message() -> LemmyResult<()> { let reg_banned = LemmyErrorType::PersonIsBannedFromSite(String::from("reason")); let err = LemmyError::from(reg_banned).error_response(); - let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); + let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?; assert_eq!( &json, "{\"error\":\"person_is_banned_from_site\",\"message\":\"reason\"}" - ) + ); + + Ok(()) } #[test] @@ -328,19 +331,22 @@ cfg_if! { /// Check if errors match translations. Disabled because many are not translated at all. #[test] #[ignore] - fn test_translations_match() { + fn test_translations_match() -> LemmyResult<()> { #[derive(Deserialize)] struct Err { error: String, } - let translations = read_to_string("translations/translations/en.json").unwrap(); - LemmyErrorType::iter().for_each(|e| { - let msg = serde_json::to_string(&e).unwrap(); - let msg: Err = serde_json::from_str(&msg).unwrap(); + let translations = read_to_string("translations/translations/en.json")?; + + for e in LemmyErrorType::iter() { + let msg = serde_json::to_string(&e)?; + let msg: Err = serde_json::from_str(&msg)?; let msg = msg.error; assert!(translations.contains(&format!("\"{msg}\"")), "{msg}"); - }); + } + + Ok(()) } } } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 5a5e76d2a..7f0691496 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -29,6 +29,8 @@ pub const CACHE_DURATION_FEDERATION: Duration = Duration::from_secs(60); pub const CACHE_DURATION_API: Duration = Duration::from_secs(1); +pub const MAX_COMMENT_DEPTH_LIMIT: usize = 50; + #[macro_export] macro_rules! location_info { () => { diff --git a/crates/utils/src/rate_limit/mod.rs b/crates/utils/src/rate_limit/mod.rs index de64bac46..a6cf92150 100644 --- a/crates/utils/src/rate_limit/mod.rs +++ b/crates/utils/src/rate_limit/mod.rs @@ -221,8 +221,6 @@ fn parse_ip(addr: &str) -> Option { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { #[test] diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index a3c6f6a27..01d379986 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -136,7 +136,6 @@ impl MapLevel for Map { .entry(addr_part) .or_insert(RateLimitedGroup::new(now, adjusted_configs)); - #[allow(clippy::indexing_slicing)] let total_passes = group.check_total(action_type, now, adjusted_configs[action_type]); let children_pass = group.children.check( @@ -161,7 +160,6 @@ impl MapLevel for Map { // Evaluated if `some_children_remaining` is false let total_has_refill_in_future = || { group.total.into_iter().any(|(action_type, bucket)| { - #[allow(clippy::indexing_slicing)] let config = configs[action_type]; bucket.update(now, config).tokens != config.capacity }) @@ -214,7 +212,6 @@ impl RateLimitedGroup { now: InstantSecs, config: BucketConfig, ) -> bool { - #[allow(clippy::indexing_slicing)] // `EnumMap` has no `get` function let bucket = &mut self.total[action_type]; let new_bucket = bucket.update(now, config); @@ -311,11 +308,10 @@ fn split_ipv6(ip: Ipv6Addr) -> ([u8; 6], u8, u8) { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::{ActionType, BucketConfig, InstantSecs, RateLimitState, RateLimitedGroup}; + use crate::error::LemmyResult; use pretty_assertions::assert_eq; #[test] @@ -330,7 +326,7 @@ mod tests { } #[test] - fn test_rate_limiter() { + fn test_rate_limiter() -> LemmyResult<()> { let bucket_configs = enum_map::enum_map! { ActionType::Message => BucketConfig { capacity: 2, @@ -354,14 +350,13 @@ mod tests { "1:2:3:0405:6::", ]; for ip in ips { - let ip = ip.parse().unwrap(); + let ip = ip.parse()?; let message_passed = rate_limiter.check(ActionType::Message, ip, now); let post_passed = rate_limiter.check(ActionType::Post, ip, now); assert!(message_passed); assert!(post_passed); } - #[allow(clippy::indexing_slicing)] let expected_buckets = |factor: u32, tokens_consumed: u32| { let adjusted_configs = bucket_configs.map(|_, config| BucketConfig { capacity: config.capacity.saturating_mul(factor), @@ -412,7 +407,7 @@ mod tests { // Do 2 `Message` actions for 1 IP address and expect only the 2nd one to fail for expected_to_pass in [true, false] { - let ip = "1:2:3:0400::".parse().unwrap(); + let ip = "1:2:3:0400::".parse()?; let passed = rate_limiter.check(ActionType::Message, ip, now); assert_eq!(passed, expected_to_pass); } @@ -424,7 +419,7 @@ mod tests { assert!(rate_limiter.ipv6_buckets.is_empty()); // `remove full buckets` should not remove empty buckets - let ip = "1.1.1.1".parse().unwrap(); + let ip = "1.1.1.1".parse()?; // empty the bucket with 2 requests assert!(rate_limiter.check(ActionType::Post, ip, now)); assert!(rate_limiter.check(ActionType::Post, ip, now)); @@ -434,11 +429,13 @@ mod tests { // `remove full buckets` should not remove partial buckets now.secs += 2; - let ip = "1.1.1.1".parse().unwrap(); + let ip = "1.1.1.1".parse()?; // Only make one request, so bucket still has 1 token assert!(rate_limiter.check(ActionType::Post, ip, now)); rate_limiter.remove_full_buckets(now); assert!(!rate_limiter.ipv4_buckets.is_empty()); + + Ok(()) } } diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 547ae20d9..8c28d908a 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -90,6 +90,10 @@ pub struct PictrsConfig { /// Timeout for uploading images to pictrs (in seconds) #[default(30)] pub upload_timeout: u64, + + /// Resize post thumbnails to this maximum width/height. + #[default(256)] + pub max_thumbnail_size: u32, } #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document, PartialEq)] diff --git a/crates/utils/src/utils/markdown/image_links.rs b/crates/utils/src/utils/markdown/image_links.rs new file mode 100644 index 000000000..a21bb6f41 --- /dev/null +++ b/crates/utils/src/utils/markdown/image_links.rs @@ -0,0 +1,168 @@ +use super::{link_rule::Link, MARKDOWN_PARSER}; +use crate::settings::SETTINGS; +use markdown_it::{plugins::cmark::inline::image::Image, NodeValue}; +use url::Url; +use urlencoding::encode; + +/// Rewrites all links to remote domains in markdown, so they go through `/api/v3/image_proxy`. +pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { + let links_offsets = find_urls::(&src); + + let mut links = vec![]; + // Go through the collected links in reverse order + for (start, end) in links_offsets.into_iter().rev() { + let (url, extra) = markdown_handle_title(&src, start, end); + match Url::parse(url) { + Ok(parsed) => { + links.push(parsed.clone()); + // If link points to remote domain, replace with proxied link + if parsed.domain() != Some(&SETTINGS.hostname) { + let mut proxied = format!( + "{}/api/v3/image_proxy?url={}", + SETTINGS.get_protocol_and_hostname(), + encode(url), + ); + // restore custom emoji format + if let Some(extra) = extra { + proxied = format!("{proxied} {extra}"); + } + src.replace_range(start..end, &proxied); + } + } + Err(_) => { + // If its not a valid url, replace with empty text + src.replace_range(start..end, ""); + } + } + } + + (src, links) +} + +pub fn markdown_handle_title(src: &str, start: usize, end: usize) -> (&str, Option<&str>) { + let content = src.get(start..end).unwrap_or_default(); + // necessary for custom emojis which look like `![name](url "title")` + let (url, extra) = if content.contains(' ') { + let split = content.split_once(' ').expect("split is valid"); + (split.0, Some(split.1)) + } else { + (content, None) + }; + (url, extra) +} + +pub fn markdown_find_links(src: &str) -> Vec<(usize, usize)> { + find_urls::(src) +} + +// Walk the syntax tree to find positions of image or link urls +fn find_urls(src: &str) -> Vec<(usize, usize)> { + let ast = MARKDOWN_PARSER.parse(src); + let mut links_offsets = vec![]; + ast.walk(|node, _depth| { + if let Some(image) = node.cast::() { + let node_offsets = node.srcmap.expect("srcmap is none").get_byte_offsets(); + let start_offset = node_offsets.1 - image.url_len() - 1 - image.title_len(); + let end_offset = node_offsets.1 - 1; + + links_offsets.push((start_offset, end_offset)); + } + }); + links_offsets +} + +pub trait UrlAndTitle { + fn url_len(&self) -> usize; + fn title_len(&self) -> usize; +} + +impl UrlAndTitle for Image { + fn url_len(&self) -> usize { + self.url.len() + } + + fn title_len(&self) -> usize { + self.title.as_ref().map(|t| t.len() + 3).unwrap_or_default() + } +} +impl UrlAndTitle for Link { + fn url_len(&self) -> usize { + self.url.len() + } + fn title_len(&self) -> usize { + self.title.as_ref().map(|t| t.len() + 3).unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_find_links() { + let links = markdown_find_links("[test](https://example.com)"); + assert_eq!(vec![(7, 26)], links); + + let links = find_urls::("![test](https://example.com)"); + assert_eq!(vec![(8, 27)], links); + } + + #[test] + fn test_markdown_proxy_images() { + let tests: Vec<_> = + vec![ + ( + "remote image proxied", + "![link](http://example.com/image.jpg)", + "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", + ), + ( + "local image unproxied", + "![link](http://lemmy-alpha/image.jpg)", + "![link](http://lemmy-alpha/image.jpg)", + ), + ( + "multiple image links", + "![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)", + "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", + ), + ( + "empty link handled", + "![image]()", + "![image]()" + ), + ( + "empty label handled", + "![](http://example.com/image.jpg)", + "![](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" + ), + ( + "invalid image link removed", + "![image](http-not-a-link)", + "![image]()" + ), + ( + "label with nested markdown handled", + "![a *b* c](http://example.com/image.jpg)", + "![a *b* c](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" + ), + ( + "custom emoji support", + r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#, + r#"![party-blob](https://lemmy-alpha/api/v3/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# + ) + ]; + + tests.iter().for_each(|&(msg, input, expected)| { + let result = markdown_rewrite_image_links(input.to_string()); + + assert_eq!( + result.0, expected, + "Testing {}, with original input '{}'", + msg, input + ); + }); + } +} diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs index 7ed553e06..a6de096e3 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -1,10 +1,9 @@ -use crate::{error::LemmyResult, settings::SETTINGS, LemmyErrorType}; -use markdown_it::{plugins::cmark::inline::image::Image, MarkdownIt}; +use crate::{error::LemmyResult, LemmyErrorType}; +use markdown_it::MarkdownIt; use regex::RegexSet; use std::sync::LazyLock; -use url::Url; -use urlencoding::encode; +pub mod image_links; mod link_rule; mod spoiler_rule; @@ -35,70 +34,6 @@ pub fn markdown_to_html(text: &str) -> String { MARKDOWN_PARSER.parse(text).xrender() } -/// Rewrites all links to remote domains in markdown, so they go through `/api/v3/image_proxy`. -pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { - let ast = MARKDOWN_PARSER.parse(&src); - let mut links_offsets = vec![]; - - // Walk the syntax tree to find positions of image links - ast.walk(|node, _depth| { - if let Some(image) = node.cast::() { - // srcmap is always present for image - // https://github.com/markdown-it-rust/markdown-it/issues/36#issuecomment-1777844387 - let node_offsets = node.srcmap.expect("srcmap is none").get_byte_offsets(); - // necessary for custom emojis which look like `![name](url "title")` - let start_offset = node_offsets.1 - - image.url.len() - - 1 - - image - .title - .as_ref() - .map(|t| t.len() + 3) - .unwrap_or_default(); - let end_offset = node_offsets.1 - 1; - - links_offsets.push((start_offset, end_offset)); - } - }); - - let mut links = vec![]; - // Go through the collected links in reverse order - while let Some((start, end)) = links_offsets.pop() { - let content = src.get(start..end).unwrap_or_default(); - // necessary for custom emojis which look like `![name](url "title")` - let (url, extra) = if content.contains(' ') { - let split = content.split_once(' ').expect("split is valid"); - (split.0, Some(split.1)) - } else { - (content, None) - }; - match Url::parse(url) { - Ok(parsed) => { - links.push(parsed.clone()); - // If link points to remote domain, replace with proxied link - if parsed.domain() != Some(&SETTINGS.hostname) { - let mut proxied = format!( - "{}/api/v3/image_proxy?url={}", - SETTINGS.get_protocol_and_hostname(), - encode(url), - ); - // restore custom emoji format - if let Some(extra) = extra { - proxied = format!("{proxied} {extra}"); - } - src.replace_range(start..end, &proxied); - } - } - Err(_) => { - // If its not a valid url, replace with empty text - src.replace_range(start..end, ""); - } - } - } - - (src, links) -} - pub fn markdown_check_for_blocked_urls(text: &str, blocklist: &RegexSet) -> LemmyResult<()> { if blocklist.is_match(text) { Err(LemmyErrorType::BlockedUrl)? @@ -107,11 +42,10 @@ pub fn markdown_check_for_blocked_urls(text: &str, blocklist: &RegexSet) -> Lemm } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; + use image_links::markdown_rewrite_image_links; use pretty_assertions::assert_eq; #[test] @@ -246,8 +180,8 @@ mod tests { } #[test] - fn test_url_blocking() { - let set = RegexSet::new(vec![r"(https://)?example\.com/?"]).unwrap(); + fn test_url_blocking() -> LemmyResult<()> { + let set = RegexSet::new(vec![r"(https://)?example\.com/?"])?; assert!( markdown_check_for_blocked_urls(&String::from("[](https://example.com)"), &set).is_err() @@ -275,7 +209,7 @@ mod tests { ) .is_err()); - let set = RegexSet::new(vec![r"(https://)?example\.com/spam\.jpg"]).unwrap(); + let set = RegexSet::new(vec![r"(https://)?example\.com/spam\.jpg"])?; assert!(markdown_check_for_blocked_urls( &String::from("![](https://example.com/spam.jpg)"), &set @@ -286,8 +220,7 @@ mod tests { r"(https://)?quo\.example\.com/?", r"(https://)?foo\.example\.com/?", r"(https://)?bar\.example\.com/?", - ]) - .unwrap(); + ])?; assert!( markdown_check_for_blocked_urls(&String::from("https://baz.example.com"), &set).is_ok() @@ -297,15 +230,17 @@ mod tests { markdown_check_for_blocked_urls(&String::from("https://bar.example.com"), &set).is_err() ); - let set = RegexSet::new(vec![r"(https://)?example\.com/banned_page"]).unwrap(); + let set = RegexSet::new(vec![r"(https://)?example\.com/banned_page"])?; assert!( markdown_check_for_blocked_urls(&String::from("https://example.com/page"), &set).is_ok() ); - let set = RegexSet::new(vec![r"(https://)?ex\.mple\.com/?"]).unwrap(); + let set = RegexSet::new(vec![r"(https://)?ex\.mple\.com/?"])?; assert!(markdown_check_for_blocked_urls("example.com", &set).is_ok()); + + Ok(()) } #[test] diff --git a/crates/utils/src/utils/markdown/spoiler_rule.rs b/crates/utils/src/utils/markdown/spoiler_rule.rs index caced310a..fd6450b31 100644 --- a/crates/utils/src/utils/markdown/spoiler_rule.rs +++ b/crates/utils/src/utils/markdown/spoiler_rule.rs @@ -134,8 +134,6 @@ pub fn add(markdown_parser: &mut MarkdownIt) { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::utils::markdown::spoiler_rule::add; diff --git a/crates/utils/src/utils/mention.rs b/crates/utils/src/utils/mention.rs index c7cc2043f..13762ed27 100644 --- a/crates/utils/src/utils/mention.rs +++ b/crates/utils/src/utils/mention.rs @@ -34,8 +34,7 @@ pub fn scrape_text_for_mentions(text: &str) -> Vec { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] +#[expect(clippy::indexing_slicing)] mod test { use crate::utils::mention::scrape_text_for_mentions; diff --git a/crates/utils/src/utils/slurs.rs b/crates/utils/src/utils/slurs.rs index ba94372fa..2350822eb 100644 --- a/crates/utils/src/utils/slurs.rs +++ b/crates/utils/src/utils/slurs.rs @@ -61,17 +61,18 @@ pub(crate) fn slurs_vec_to_str(slurs: &[&str]) -> String { } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod test { - use crate::utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str}; + use crate::{ + error::LemmyResult, + utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str}, + }; use pretty_assertions::assert_eq; use regex::RegexBuilder; #[test] - fn test_slur_filter() { - let slur_regex = Some(RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap()); + fn test_slur_filter() -> LemmyResult<()> { + let slur_regex = Some(RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build()?); let test = "faggot test kike tranny cocksucker retardeds. Capitalized Niggerz. This is a bunch of other safe text."; let slur_free = "No slurs here"; @@ -96,6 +97,8 @@ mod test { if let Err(slur_vec) = slur_check(test, &slur_regex) { assert_eq!(&slurs_vec_to_str(&slur_vec), has_slurs_err_str); } + + Ok(()) } // These helped with testing diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index 9493ca2fc..98b9a2a25 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -351,7 +351,6 @@ pub fn build_url_str_without_scheme(url_str: &str) -> LemmyResult { } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::{ diff --git a/docker/Dockerfile b/docker/Dockerfile index 68490bfd9..4503dd402 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.9 +# syntax=docker/dockerfile:1.10 ARG RUST_VERSION=1.81 ARG CARGO_BUILD_FEATURES=default ARG RUST_RELEASE_MODE=debug diff --git a/migrations/2024-09-16-174833_create_oauth_provider/up.sql b/migrations/2024-09-16-174833_create_oauth_provider/up.sql index a75f01228..308d86cce 100644 --- a/migrations/2024-09-16-174833_create_oauth_provider/up.sql +++ b/migrations/2024-09-16-174833_create_oauth_provider/up.sql @@ -14,7 +14,7 @@ CREATE TABLE oauth_provider ( scopes text NOT NULL, auto_verify_email boolean DEFAULT TRUE NOT NULL, account_linking_enabled boolean DEFAULT FALSE NOT NULL, - enabled boolean DEFAULT FALSE NOT NULL, + enabled boolean DEFAULT TRUE NOT NULL, published timestamp with time zone DEFAULT now() NOT NULL, updated timestamp with time zone ); diff --git a/migrations/2024-09-20-134838_add_federation_vote_rejection/down.sql b/migrations/2024-09-20-134838_add_federation_vote_rejection/down.sql new file mode 100644 index 000000000..a9181ca4a --- /dev/null +++ b/migrations/2024-09-20-134838_add_federation_vote_rejection/down.sql @@ -0,0 +1,31 @@ +-- Add back the enable_downvotes column +ALTER TABLE local_site + ADD COLUMN enable_downvotes boolean DEFAULT TRUE NOT NULL; + +-- regenerate their values (from post_downvotes alone) +WITH subquery AS ( + SELECT + post_downvotes, + CASE WHEN post_downvotes = 'Disable'::federation_mode_enum THEN + FALSE + ELSE + TRUE + END + FROM + local_site) +UPDATE + local_site +SET + enable_downvotes = subquery.case +FROM + subquery; + +-- Drop the new columns +ALTER TABLE local_site + DROP COLUMN post_upvotes, + DROP COLUMN post_downvotes, + DROP COLUMN comment_upvotes, + DROP COLUMN comment_downvotes; + +DROP TYPE federation_mode_enum; + diff --git a/migrations/2024-09-20-134838_add_federation_vote_rejection/up.sql b/migrations/2024-09-20-134838_add_federation_vote_rejection/up.sql new file mode 100644 index 000000000..a2ee6ad4d --- /dev/null +++ b/migrations/2024-09-20-134838_add_federation_vote_rejection/up.sql @@ -0,0 +1,39 @@ +-- This removes the simple enable_downvotes setting, in favor of an +-- expanded federation mode type for post/comment up/downvotes. +-- Create the federation mode enum +CREATE TYPE federation_mode_enum AS ENUM ( + 'All', + 'Local', + 'Disable' +); + +-- Add the new columns +ALTER TABLE local_site + ADD COLUMN post_upvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL, + ADD COLUMN post_downvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL, + ADD COLUMN comment_upvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL, + ADD COLUMN comment_downvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL; + +-- Copy over the enable_downvotes into the post and comment downvote settings +WITH subquery AS ( + SELECT + enable_downvotes, + CASE WHEN enable_downvotes = TRUE THEN + 'All'::federation_mode_enum + ELSE + 'Disable'::federation_mode_enum + END + FROM + local_site) +UPDATE + local_site +SET + post_downvotes = subquery.case, + comment_downvotes = subquery.case +FROM + subquery; + +-- Drop the enable_downvotes column +ALTER TABLE local_site + DROP COLUMN enable_downvotes; + diff --git a/migrations/2024-09-23-133038_remove_auto_expand/down.sql b/migrations/2024-09-23-133038_remove_auto_expand/down.sql new file mode 100644 index 000000000..04e0155d0 --- /dev/null +++ b/migrations/2024-09-23-133038_remove_auto_expand/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + ADD COLUMN auto_expand boolean NOT NULL DEFAULT FALSE; + diff --git a/migrations/2024-09-23-133038_remove_auto_expand/up.sql b/migrations/2024-09-23-133038_remove_auto_expand/up.sql new file mode 100644 index 000000000..35d706d1b --- /dev/null +++ b/migrations/2024-09-23-133038_remove_auto_expand/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + DROP COLUMN auto_expand; + diff --git a/renovate.json b/renovate.json index 8d57f0aa8..03619c85f 100644 --- a/renovate.json +++ b/renovate.json @@ -2,5 +2,18 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], "schedule": ["before 4am on the first day of the month"], - "automerge": true + "automerge": true, + "rebaseWhen": "conflicted", + "packageRules": [ + { + "groupName": "docker", + "matchDatasources": ["docker"] + }, + { + "groupName": "npm", + "matchDatasources": ["npm"] + } + ], + "ignoreDeps": ["lemmy-js-client", "pgautoupgrade/pgautoupgrade"], + "ignorePaths": ["(^|/)Cargo\\.toml$"] } diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 65931e810..df1aebf84 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -17,6 +17,7 @@ use lemmy_api::{ block::block_community, follow::follow_community, hide::hide_community, + random::get_random_community, transfer::transfer_community, }, local_user::{ @@ -193,6 +194,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .wrap(rate_limit.message()) .route("", web::get().to(get_community)) .route("", web::put().to(update_community)) + .route("/random", web::get().to(get_random_community)) .route("/hide", web::put().to(hide_community)) .route("/list", web::get().to(list_communities)) .route("/follow", web::post().to(follow_community)) diff --git a/src/lib.rs b/src/lib.rs index 804ac7aa1..4aee4be69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,9 +122,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { run_advanced_migrations(&mut (&pool).into(), &SETTINGS).await?; // Initialize the secrets - let secret = Secret::init(&mut (&pool).into()) - .await? - .expect("Couldn't initialize secrets."); + let secret = Secret::init(&mut (&pool).into()).await?; // Make sure the local site is set up. let site_view = SiteView::read_local(&mut (&pool).into()).await?; diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index b7532d83c..2f99fe8a1 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -605,7 +605,6 @@ async fn build_update_instance_form( } #[cfg(test)] -#[allow(clippy::indexing_slicing)] mod tests { use crate::scheduled_tasks::build_update_instance_form; diff --git a/src/session_middleware.rs b/src/session_middleware.rs index b23f8644e..ec8f4399c 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -97,8 +97,6 @@ where } #[cfg(test)] -#[allow(clippy::unwrap_used)] -#[allow(clippy::indexing_slicing)] mod tests { use super::*; @@ -114,7 +112,7 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; - use lemmy_utils::rate_limit::RateLimitCell; + use lemmy_utils::{error::LemmyResult, rate_limit::RateLimitCell}; use pretty_assertions::assert_eq; use reqwest::Client; use reqwest_middleware::ClientBuilder; @@ -123,13 +121,15 @@ mod tests { #[tokio::test] #[serial] - async fn test_session_auth() { + async fn test_session_auth() -> LemmyResult<()> { // hack, necessary so that config file can be loaded from hardcoded, relative path - set_current_dir("crates/utils").unwrap(); + set_current_dir("crates/utils")?; let pool_ = build_db_pool_for_tests().await; let pool = &mut (&pool_).into(); - let secret = Secret::init(pool).await.unwrap().unwrap(); + + let secret = Secret::init(pool).await?; + let context = LemmyContext::create( pool_.clone(), ClientBuilder::new(Client::default()).build(), @@ -137,29 +137,25 @@ mod tests { RateLimitCell::with_test_config(), ); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); - let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) - .await - .unwrap(); + let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; let req = TestRequest::default().to_http_request(); - let jwt = Claims::generate(inserted_local_user.id, req, &context) - .await - .unwrap(); + let jwt = Claims::generate(inserted_local_user.id, req, &context).await?; let valid = Claims::validate(&jwt, &context).await; assert!(valid.is_ok()); - let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + let num_deleted = Person::delete(pool, inserted_person.id).await?; assert_eq!(1, num_deleted); + + Ok(()) } }