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..3cfec53eb 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", @@ -2809,9 +2809,13 @@ dependencies = [ "itertools 0.13.0", "lettre", "markdown-it", + "markdown-it-block-spoiler", + "markdown-it-ruby", + "markdown-it-sub", + "markdown-it-sup", "pretty_assertions", "regex", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "rosetta-build", "rosetta-i18n", @@ -2847,7 +2851,7 @@ dependencies = [ "nom", "percent-encoding", "quoted_printable", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pemfile 2.1.3", "rustls-pki-types", "socket2", @@ -2980,6 +2984,44 @@ dependencies = [ "unicode-general-category", ] +[[package]] +name = "markdown-it-block-spoiler" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008a8e4184fd08b5dca0f2b5b2ef8f126c1e83ca797c44ee41f8d7765951360c" +dependencies = [ + "itertools 0.13.0", + "markdown-it", +] + +[[package]] +name = "markdown-it-ruby" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3505f4ada7c372e7f5eb4b07850bf5921193bc0bd43cb18991233999c9134d4" +dependencies = [ + "itertools 0.13.0", + "markdown-it", +] + +[[package]] +name = "markdown-it-sub" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abe3aa8927af2314644b3aae37393241a229e869ff9c95ac640749e08357d2a" +dependencies = [ + "markdown-it", +] + +[[package]] +name = "markdown-it-sup" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ae949e78c7a615f88a47019d51b65962bfc5c4cbc65fa81eae8b9b2506d1cb1" +dependencies = [ + "markdown-it", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -3762,7 +3804,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.14", "socket2", "thiserror", "tokio", @@ -3779,7 +3821,7 @@ dependencies = [ "rand", "ring", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.14", "slab", "thiserror", "tinyvec", @@ -3875,14 +3917,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 +3938,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 +3961,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 +4008,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 +4032,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 +4061,7 @@ dependencies = [ "anyhow", "async-trait", "http 1.1.0", - "reqwest 0.12.7", + "reqwest 0.12.8", "serde", "thiserror", "tower-service", @@ -4036,7 +4078,7 @@ dependencies = [ "getrandom", "http 1.1.0", "matchit", - "reqwest 0.12.7", + "reqwest 0.12.8", "reqwest-middleware", "tracing", ] @@ -4177,9 +4219,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 +4254,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 +4415,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 +4433,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 +4779,7 @@ dependencies = [ "fnv", "once_cell", "plist", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "serde", "serde_derive", "serde_json", @@ -4974,7 +5016,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 +5039,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", ] diff --git a/Cargo.toml b/Cargo.toml index 6b594a140..fc22d0d53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 f28fd7689..4b09f7619 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 f3895a666..79a5ccbf1 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.12 version: 0.20.0-alpha.12 @@ -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 5f2059e4f..153405820 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -158,16 +158,16 @@ test("Delete a comment", async () => { expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); expect(deleteCommentRes.comment_view.comment.content).toBe(""); - // Make sure that comment is undefined on beta + // Make sure that comment is deleted on beta await waitUntil( - () => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), - e => e.message == "not_found", + () => resolveComment(beta, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === true, ); - // Make sure that comment is undefined on gamma after delete + // Make sure that comment is deleted on gamma after delete await waitUntil( - () => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e), - e => e.message === "not_found", + () => resolveComment(gamma, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === true, ); // Test undeleting the comment @@ -181,11 +181,10 @@ test("Delete a comment", async () => { // Make sure that comment is undeleted on beta let betaComment2 = ( await waitUntil( - () => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), - e => e.message !== "not_found", + () => resolveComment(beta, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === false, ) ).comment; - expect(betaComment2?.comment.deleted).toBe(false); assertCommentFederation(betaComment2, undeleteCommentRes.comment_view); }); 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..e93b8513f 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, @@ -61,7 +67,6 @@ pub async fn like_comment( let like_form = CommentLikeForm { comment_id: data.comment_id, - post_id: orig_comment.post.id, person_id: local_user_view.person.id, score: data.score, }; 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/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/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 f5ddec35b..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, diff --git a/crates/api/src/site/registration_applications/tests.rs b/crates/api/src/site/registration_applications/tests.rs index dd6c0694a..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; -#[expect(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)) } -#[expect(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, ) diff --git a/crates/api/src/sitemap.rs b/crates/api/src/sitemap.rs index 2d06a1249..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 10bd7a0e3..6476f855a 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -69,7 +69,6 @@ impl Claims { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::{claims::Claims, context::LemmyContext}; @@ -84,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; @@ -92,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(), @@ -103,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/request.rs b/crates/api_common/src/request.rs index 006f6b52a..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,13 +472,13 @@ pub async fn replace_image( } #[cfg(test)] -#[expect(clippy::unwrap_used)] 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; @@ -485,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 @@ -499,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 @@ -510,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 @@ -530,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 @@ -542,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 @@ -550,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 d82303327..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, @@ -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)] diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 682b61063..eca6c0c41 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::{ @@ -48,9 +50,12 @@ use lemmy_utils::{ email::{send_email, translations::Lang}, error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, rate_limit::{ActionType, BucketConfig}, - settings::structs::{PictrsImageMode, Settings}, + settings::{ + structs::{PictrsImageMode, Settings}, + 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 +301,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 +695,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 +893,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(()) } @@ -872,12 +976,8 @@ pub fn generate_followers_url(actor_id: &DbUrl) -> Result { Ok(Url::parse(&format!("{actor_id}/followers"))?.into()) } -pub fn generate_inbox_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/inbox"))?.into()) -} - -pub fn generate_shared_inbox_url(settings: &Settings) -> LemmyResult { - let url = format!("{}/inbox", settings.get_protocol_and_hostname()); +pub fn generate_inbox_url() -> LemmyResult { + let url = format!("{}/inbox", SETTINGS.get_protocol_and_hostname()); Ok(Url::parse(&url)?.into()) } @@ -1067,10 +1167,20 @@ fn build_proxied_image_url( } #[cfg(test)] -#[expect(clippy::unwrap_used)] 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; @@ -1092,48 +1202,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() @@ -1146,5 +1250,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 273ab7a5f..2f67fa7e7 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -88,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(), @@ -108,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()) }; @@ -136,7 +132,6 @@ pub async fn create_comment( // You like your own comment by default let like_form = CommentLikeForm { comment_id: inserted_comment.id, - post_id: post.id, person_id: local_user_view.person.id, score: 1, }; 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/community/create.rs b/crates/api_crud/src/community/create.rs index 4e3e356e0..f02d733e5 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -8,7 +8,6 @@ use lemmy_api_common::{ generate_followers_url, generate_inbox_url, generate_local_apub_endpoint, - generate_shared_inbox_url, get_url_blocklist, is_admin, local_site_to_slur_regex, @@ -96,8 +95,7 @@ pub async fn create_community( actor_id: Some(community_actor_id.clone()), private_key: Some(keypair.private_key), followers_url: Some(generate_followers_url(&community_actor_id)?), - inbox_url: Some(generate_inbox_url(&community_actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + inbox_url: Some(generate_inbox_url()?), posting_restricted_to_mods: data.posting_restricted_to_mods, visibility: data.visibility, ..CommunityInsertForm::new( 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 9b0439da8..733837de7 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ context::LemmyContext, site::{CreateSite, SiteResponse}, utils::{ - generate_shared_inbox_url, + generate_inbox_url, get_url_blocklist, is_admin, local_site_rate_limit_to_rate_limit_config, @@ -55,7 +55,7 @@ pub async fn create_site( validate_create_payload(&local_site, &data)?; let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into(); - let inbox_url = Some(generate_shared_inbox_url(context.settings())?); + let inbox_url = Some(generate_inbox_url()?); let keypair = generate_actor_keypair()?; let slur_regex = local_site_to_slur_regex(&local_site); @@ -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() }; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 20c10c415..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()); diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 495a5cc98..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() }; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index bf17d6f8e..ed560e3d6 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -11,7 +11,6 @@ use lemmy_api_common::{ check_user_valid, generate_inbox_url, generate_local_apub_endpoint, - generate_shared_inbox_url, honeypot_check, local_site_to_slur_regex, password_length_check, @@ -418,8 +417,7 @@ async fn create_person( // Register the new person let person_form = PersonInsertForm { actor_id: Some(actor_id.clone()), - inbox_url: Some(generate_inbox_url(&actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + inbox_url: Some(generate_inbox_url()?), private_key: Some(actor_keypair.private_key), ..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id) }; 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/community/update.rs b/crates/apub/src/activities/community/update.rs index f507b3425..48a64bd9d 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -106,8 +106,14 @@ impl ActivityHandler for UpdateCommunity { icon: Some(self.object.icon.map(|i| i.url.into())), banner: Some(self.object.image.map(|i| i.url.into())), followers_url: self.object.followers.map(Into::into), - inbox_url: Some(self.object.inbox.into()), - shared_inbox_url: Some(self.object.endpoints.map(|e| e.shared_inbox.into())), + inbox_url: Some( + self + .object + .endpoints + .map(|e| e.shared_inbox) + .unwrap_or(self.object.inbox) + .into(), + ), moderators_url: self.object.attributed_to.map(Into::into), posting_restricted_to_mods: self.object.posting_restricted_to_mods, featured_url: self.object.featured.map(Into::into), diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index b2c436049..0a0737151 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -153,7 +153,6 @@ impl ActivityHandler for CreateOrUpdateNote { // author likes their own comment by default let like_form = CommentLikeForm { comment_id: comment.id, - post_id: comment.post_id, person_id: comment.creator_id, score: 1, }; diff --git a/crates/apub/src/activities/voting/mod.rs b/crates/apub/src/activities/voting/mod.rs index 3e59cb7d0..7c39b2246 100644 --- a/crates/apub/src/activities/voting/mod.rs +++ b/crates/apub/src/activities/voting/mod.rs @@ -62,7 +62,6 @@ async fn vote_comment( let comment_id = comment.id; let like_form = CommentLikeForm { comment_id, - post_id: comment.post_id, person_id: actor.id, score: vote_type.into(), }; 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/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index b3061d1ce..d9d50e69e 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -1,10 +1,10 @@ use crate::fetcher::{ + post_or_comment::PostOrComment, search::{search_query_to_object_id, search_query_to_object_id_local, SearchableObjects}, user_or_community::UserOrCommunity, }; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; -use diesel::NotFound; use lemmy_api_common::{ context::LemmyContext, site::{ResolveObject, ResolveObjectResponse}, @@ -46,35 +46,145 @@ 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); + let is_admin = local_user.clone().map(|l| l.admin).unwrap_or_default(); 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 { - UserOrCommunity::User(u) => { - removed_or_deleted = u.deleted; - res.person = Some(PersonView::read(pool, u.id).await?) + SearchableObjects::PostOrComment(pc) => match *pc { + PostOrComment::Post(p) => { + res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), is_admin).await?) } + PostOrComment::Comment(c) => { + res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?) + } + }, + SearchableObjects::PersonOrCommunity(pc) => match *pc { + UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id).await?), UserOrCommunity::Community(c) => { - removed_or_deleted = c.deleted || c.removed; - res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), false).await?) + res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?) } }, }; - // if the object was deleted from database, dont return it - if removed_or_deleted { - Err(NotFound {}.into()) - } else { - Ok(Json(res)) + + Ok(Json(res)) +} + +#[cfg(test)] +mod tests { + use crate::api::resolve_object::resolve_object; + use actix_web::web::Query; + use lemmy_api_common::{context::LemmyContext, site::ResolveObject}; + use lemmy_db_schema::{ + source::{ + community::{Community, CommunityInsertForm}, + instance::Instance, + local_site::{LocalSite, LocalSiteInsertForm}, + post::{Post, PostInsertForm, PostUpdateForm}, + site::{Site, SiteInsertForm}, + }, + traits::Crud, + }; + use lemmy_db_views::structs::LocalUserView; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + use serial_test::serial; + + #[tokio::test] + #[serial] + #[expect(clippy::unwrap_used)] + async fn test_object_visibility() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); + + let name = "test_local_user_name"; + let bio = "test_local_user_bio"; + + let creator = LocalUserView::create_test_user(pool, name, bio, false).await?; + let regular_user = LocalUserView::create_test_user(pool, name, bio, false).await?; + let admin_user = LocalUserView::create_test_user(pool, name, bio, true).await?; + + let instance_id = creator.person.instance_id; + let site_form = SiteInsertForm::new("test site".to_string(), instance_id); + let site = Site::create(pool, &site_form).await?; + + let local_site_form = LocalSiteInsertForm { + site_setup: Some(true), + private_instance: Some(false), + ..LocalSiteInsertForm::new(site.id) + }; + LocalSite::create(pool, &local_site_form).await?; + + let community = Community::create( + pool, + &CommunityInsertForm::new( + instance_id, + "test".to_string(), + "test".to_string(), + "pubkey".to_string(), + ), + ) + .await?; + + let post_insert_form = PostInsertForm::new("Test".to_string(), creator.person.id, community.id); + let post = Post::create(pool, &post_insert_form).await?; + + let query = format!("q={}", post.ap_id).to_string(); + let query: Query = Query::from_query(&query)?; + + // Objects should be resolvable without authentication + let res = resolve_object(query.clone(), context.reset_request_count(), None).await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + // Objects should be resolvable by regular users + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(regular_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + // Objects should be resolvable by admins + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(admin_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + + Post::update( + pool, + post.id, + &PostUpdateForm { + deleted: Some(true), + ..Default::default() + }, + ) + .await?; + + // Deleted objects should not be resolvable without authentication + let res = resolve_object(query.clone(), context.reset_request_count(), None).await; + assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound)); + // Deleted objects should not be resolvable by regular users + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(regular_user.clone()), + ) + .await; + assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound)); + // Deleted objects should be resolvable by admins + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(admin_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + + LocalSite::delete(pool).await?; + Site::delete(pool, site.id).await?; + Instance::delete(pool, instance_id).await?; + + Ok(()) } } diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index d9ae20ede..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))] @@ -102,7 +106,7 @@ pub async fn search( }; let community_query = CommunityQuery { - sort, + sort: sort.map(CommunitySortType::from), listing_type, search_term: Some(q.clone()), title_only, diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 6a524b71d..2e075c202 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -313,18 +313,14 @@ where #[cfg(test)] #[expect(clippy::indexing_slicing)] -mod tests { - +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::{ community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - person::{Person, PersonInsertForm}, + local_user::LocalUser, }, traits::{Crud, Followable}, }; @@ -336,32 +332,13 @@ mod tests { use std::time::Duration; use tokio::time::sleep; - async fn create_user( - name: String, - bio: Option, - context: &Data, - ) -> LemmyResult { - let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?; - let person_form = PersonInsertForm { - display_name: Some(name.clone()), - bio, - ..PersonInsertForm::test_form(instance.id, &name) - }; - let person = Person::create(&mut context.pool(), &person_form).await?; - - let user_form = LocalUserInsertForm::test_form(person.id); - let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?; - - Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?) - } - #[tokio::test] #[serial] async fn test_settings_export_import() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); - let export_user = - create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; + let export_user = LocalUserView::create_test_user(pool, "hanna", "my bio", false).await?; let community_form = CommunityInsertForm::new( export_user.person.instance_id, @@ -369,25 +346,25 @@ mod tests { "testcom".to_string(), "pubkey".to_string(), ); - let community = Community::create(&mut context.pool(), &community_form).await?; + let community = Community::create(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?; + CommunityFollower::follow(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 import_user = + LocalUserView::create_test_user(pool, "charles", "charles bio", false).await?; import_settings(backup, import_user.clone(), context.reset_request_count()).await?; // wait for background task to finish sleep(Duration::from_millis(1000)).await; - let import_user_updated = - LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?; + let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?; assert_eq!( export_user.person.display_name, @@ -395,13 +372,12 @@ mod tests { ); assert_eq!(export_user.person.bio, import_user_updated.person.bio); - let follows = - CommunityFollowerView::for_person(&mut context.pool(), import_user.person.id).await?; + let follows = CommunityFollowerView::for_person(pool, import_user.person.id).await?; assert_eq!(follows.len(), 1); assert_eq!(follows[0].community.actor_id, community.actor_id); - LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?; - LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?; + LocalUser::delete(pool, export_user.local_user.id).await?; + LocalUser::delete(pool, import_user.local_user.id).await?; Ok(()) } @@ -409,9 +385,9 @@ mod tests { #[serial] async fn disallow_large_backup() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); - let export_user = - create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; + let export_user = LocalUserView::create_test_user(pool, "harry", "harry bio", false).await?; let mut backup = export_settings(export_user.clone(), context.reset_request_count()).await?; @@ -426,7 +402,7 @@ mod tests { backup.saved_comments.push("http://example4.com".parse()?); } - let import_user = create_user("charles".to_string(), None, &context).await?; + let import_user = LocalUserView::create_test_user(pool, "sally", "sally bio", false).await?; let imported = import_settings(backup, import_user.clone(), context.reset_request_count()).await; @@ -436,8 +412,8 @@ mod tests { Some(LemmyErrorType::TooManyItems) ); - LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?; - LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?; + LocalUser::delete(pool, export_user.local_user.id).await?; + LocalUser::delete(pool, import_user.local_user.id).await?; Ok(()) } @@ -445,9 +421,9 @@ mod tests { #[serial] async fn import_partial_backup() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); - let import_user = - create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; + let import_user = LocalUserView::create_test_user(pool, "larry", "larry bio", false).await?; let backup = serde_json::from_str("{\"bot_account\": true, \"settings\": {\"theme\": \"my_theme\"}}")?; @@ -458,8 +434,7 @@ mod tests { ) .await?; - let import_user_updated = - LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?; + let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?; // mark as bot account assert!(import_user_updated.person.bot_account); // dont remove existing bio diff --git a/crates/apub/src/fetcher/markdown_links.rs b/crates/apub/src/fetcher/markdown_links.rs new file mode 100644 index 000000000..d83aae515 --- /dev/null +++ b/crates/apub/src/fetcher/markdown_links.rs @@ -0,0 +1,192 @@ +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 lemmy_db_schema::{ + source::{ + community::{Community, CommunityInsertForm}, + post::{Post, PostInsertForm}, + }, + traits::Crud, + }; + use lemmy_db_views::structs::LocalUserView; + 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 = + LocalUserView::create_test_user(&mut context.pool(), "garda", "garda bio", false).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 c61d4fedc..37482aedb 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -1,5 +1,4 @@ use crate::{ - activity_lists::GroupInboxActivities, collections::{ community_featured::ApubCommunityFeatured, community_follower::ApubCommunityFollower, @@ -7,15 +6,13 @@ use crate::{ community_outbox::ApubCommunityOutbox, }, http::{check_community_public, create_apub_response, create_apub_tombstone_response}, - objects::{community::ApubCommunity, person::ApubPerson}, + objects::community::ApubCommunity, }; use activitypub_federation::{ - actix_web::inbox::receive_activity, config::Data, - protocol::context::WithContext, traits::{Collection, Object}, }; -use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; +use actix_web::{web, HttpResponse}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::ApubActor}; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; @@ -47,19 +44,6 @@ pub(crate) async fn get_apub_community_http( create_apub_response(&apub) } -/// Handler for all incoming receive to community inboxes. -#[tracing::instrument(skip_all)] -pub async fn community_inbox( - request: HttpRequest, - body: Bytes, - data: Data, -) -> LemmyResult { - receive_activity::, ApubPerson, LemmyContext>( - request, body, &data, - ) - .await -} - /// Returns an empty followers collection, only populating the size (for privacy). pub(crate) async fn get_apub_community_followers( info: web::Path, @@ -120,7 +104,6 @@ pub(crate) async fn get_apub_community_featured( } #[cfg(test)] -#[expect(clippy::unwrap_used)] pub(crate) mod tests { use super::*; @@ -182,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/http/person.rs b/crates/apub/src/http/person.rs index e8e072a97..0f628c497 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -1,17 +1,10 @@ use crate::{ - activity_lists::PersonInboxActivities, - fetcher::user_or_community::UserOrCommunity, http::{create_apub_response, create_apub_tombstone_response}, objects::person::ApubPerson, protocol::collections::empty_outbox::EmptyOutbox, }; -use activitypub_federation::{ - actix_web::inbox::receive_activity, - config::Data, - protocol::context::WithContext, - traits::Object, -}; -use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; +use activitypub_federation::{config::Data, traits::Object}; +use actix_web::{web, HttpResponse}; use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url}; use lemmy_db_schema::{source::person::Person, traits::ApubActor}; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; @@ -44,18 +37,6 @@ pub(crate) async fn get_apub_person_http( } } -#[tracing::instrument(skip_all)] -pub async fn person_inbox( - request: HttpRequest, - body: Bytes, - data: Data, -) -> LemmyResult { - receive_activity::, UserOrCommunity, LemmyContext>( - request, body, &data, - ) - .await -} - #[tracing::instrument(skip_all)] pub(crate) async fn get_apub_person_outbox( info: web::Path, diff --git a/crates/apub/src/http/routes.rs b/crates/apub/src/http/routes.rs index ab046afe1..9479e6312 100644 --- a/crates/apub/src/http/routes.rs +++ b/crates/apub/src/http/routes.rs @@ -1,7 +1,6 @@ use crate::http::{ comment::get_apub_comment, community::{ - community_inbox, get_apub_community_featured, get_apub_community_followers, get_apub_community_http, @@ -9,7 +8,7 @@ use crate::http::{ get_apub_community_outbox, }, get_activity, - person::{get_apub_person_http, get_apub_person_outbox, person_inbox}, + person::{get_apub_person_http, get_apub_person_outbox}, post::get_apub_post, shared_inbox, site::{get_apub_site_http, get_apub_site_outbox}, @@ -56,8 +55,6 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("") .guard(InboxRequestGuard) - .route("/c/{community_name}/inbox", web::post().to(community_inbox)) - .route("/u/{user_name}/inbox", web::post().to(person_inbox)) .route("/inbox", web::post().to(shared_inbox)), ); } 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..c8479eaba 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -1,10 +1,11 @@ use crate::{ activities::GetActorType, check_apub_id_valid, + fetcher::markdown_links::markdown_rewrite_remote_links_opt, local_site_data_cached, objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt}, protocol::{ - objects::{group::Group, Endpoints, LanguageTag}, + objects::{group::Group, LanguageTag}, ImageObject, Source, }, @@ -115,9 +116,7 @@ impl Object for ApubCommunity { inbox: self.inbox_url.clone().into(), outbox: generate_outbox_url(&self.actor_id)?.into(), followers: self.followers_url.clone().map(Into::into), - endpoints: self.shared_inbox_url.clone().map(|s| Endpoints { - shared_inbox: s.into(), - }), + endpoints: None, public_key: self.public_key(), language, published: Some(self.published), @@ -148,6 +147,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?; @@ -163,8 +163,13 @@ impl Object for ApubCommunity { banner, description, followers_url: group.followers.clone().map(Into::into), - inbox_url: Some(group.inbox.into()), - shared_inbox_url: group.endpoints.map(|e| e.shared_inbox.into()), + inbox_url: Some( + group + .endpoints + .map(|e| e.shared_inbox) + .unwrap_or(group.inbox) + .into(), + ), moderators_url: group.attributed_to.clone().map(Into::into), posting_restricted_to_mods: group.posting_restricted_to_mods, featured_url: group.featured.clone().map(Into::into), @@ -223,7 +228,7 @@ impl Actor for ApubCommunity { } fn shared_inbox(&self) -> Option { - self.shared_inbox_url.clone().map(Into::into) + None } } @@ -296,7 +301,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..4e8519f78 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -2,13 +2,11 @@ use super::verify_is_remote_object; use crate::{ activities::GetActorType, check_apub_id_valid_with_strictness, + fetcher::markdown_links::markdown_rewrite_remote_links_opt, local_site_data_cached, objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt}, protocol::{ - objects::{ - person::{Person, UserTypes}, - Endpoints, - }, + objects::person::{Person, UserTypes}, ImageObject, Source, }, @@ -117,9 +115,7 @@ impl Object for ApubPerson { matrix_user_id: self.matrix_user_id.clone(), published: Some(self.published), outbox: generate_outbox_url(&self.actor_id)?.into(), - endpoints: self.shared_inbox_url.clone().map(|s| Endpoints { - shared_inbox: s.into(), - }), + endpoints: None, public_key: self.public_key(), updated: self.updated, inbox: self.inbox_url.clone().into(), @@ -156,6 +152,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?; @@ -180,8 +177,13 @@ impl Object for ApubPerson { private_key: None, public_key: person.public_key.public_key_pem, last_refreshed_at: Some(naive_now()), - inbox_url: Some(person.inbox.into()), - shared_inbox_url: person.endpoints.map(|e| e.shared_inbox.into()), + inbox_url: Some( + person + .endpoints + .map(|e| e.shared_inbox) + .unwrap_or(person.inbox) + .into(), + ), matrix_user_id: person.matrix_user_id, instance_id, }; @@ -209,7 +211,7 @@ impl Actor for ApubPerson { } fn shared_inbox(&self) -> Option { - self.shared_inbox_url.clone().map(Into::into) + None } } @@ -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/db_schema/src/aggregates/comment_aggregates.rs b/crates/db_schema/src/aggregates/comment_aggregates.rs index b10c5d9db..a97bb565b 100644 --- a/crates/db_schema/src/aggregates/comment_aggregates.rs +++ b/crates/db_schema/src/aggregates/comment_aggregates.rs @@ -30,7 +30,6 @@ impl CommentAggregates { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::{ @@ -45,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, @@ -72,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, @@ -94,22 +92,17 @@ 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, - post_id: inserted_post.id, person_id: inserted_person.id, 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); @@ -118,52 +111,43 @@ mod tests { // Add a post dislike from the other person let comment_dislike = CommentLikeForm { comment_id: inserted_comment.id, - post_id: inserted_post.id, person_id: another_inserted_person.id, 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 699c4cb43..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,7 +31,6 @@ impl CommunityAggregates { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::{ @@ -51,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, @@ -78,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, @@ -86,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, @@ -94,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, @@ -104,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, @@ -114,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, @@ -138,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); @@ -153,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 f5ca95c86..6e0eacc07 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,18 +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)] -#[expect(clippy::unwrap_used)] mod tests { use crate::{ @@ -35,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, @@ -63,37 +56,36 @@ 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, person_id: inserted_person.id, - post_id: inserted_post.id, 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, @@ -101,23 +93,17 @@ 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, person_id: another_inserted_person.id, - post_id: inserted_post.id, 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); @@ -125,13 +111,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( @@ -142,8 +123,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; Comment::update( pool, inserted_child_comment.id, @@ -152,51 +132,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); @@ -204,24 +167,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 01f83ec56..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)] -#[expect(clippy::unwrap_used)] - 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 ebc1fc65d..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)] -#[expect(clippy::unwrap_used)] - 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 fef85a3ba..fff0c2f0c 100644 --- a/crates/db_schema/src/impls/activity.rs +++ b/crates/db_schema/src/impls/activity.rs @@ -58,11 +58,11 @@ impl ReceivedActivity { } #[cfg(test)] -#[expect(clippy::unwrap_used)] 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; @@ -70,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", @@ -100,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 35926de23..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,7 +388,6 @@ async fn convert_read_languages( } #[cfg(test)] -#[expect(clippy::unwrap_used)] #[expect(clippy::indexing_slicing)] mod tests { @@ -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/comment.rs b/crates/db_schema/src/impls/comment.rs index 3a787a5c9..30d18465f 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,7 +196,6 @@ impl Saveable for CommentSaved { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::{ @@ -220,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, @@ -244,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, @@ -273,8 +271,7 @@ mod tests { ap_id: Url::parse(&format!( "https://lemmy-alpha/comment/{}", inserted_comment.id - )) - .unwrap() + ))? .into(), distinguished: false, local: true, @@ -287,23 +284,19 @@ 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 { comment_id: inserted_comment.id, - post_id: inserted_post.id, person_id: inserted_person.id, 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, - post_id: inserted_post.id, person_id: inserted_person.id, published: inserted_comment_like.published, score: 1, @@ -315,7 +308,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, @@ -328,27 +321,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); @@ -362,5 +345,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 223a1dbf7..355979264 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 { @@ -498,7 +523,6 @@ mod tests { banner: None, followers_url: inserted_community.followers_url.clone(), inbox_url: inserted_community.inbox_url.clone(), - shared_inbox_url: None, moderators_url: None, featured_url: None, hidden: false, diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index e430c6fb9..cbfd14b03 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -48,19 +48,19 @@ impl FederationAllowList { } } #[cfg(test)] -#[expect(clippy::unwrap_used)] 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![ @@ -71,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()) @@ -85,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 96992a614..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)] -#[expect(clippy::unwrap_used)] #[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/moderator.rs b/crates/db_schema/src/impls/moderator.rs index ecbb38ddf..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,7 +493,6 @@ impl Crud for AdminPurgeComment { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::{ @@ -499,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, @@ -527,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 @@ -552,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, @@ -574,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, @@ -596,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, @@ -619,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, @@ -643,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, @@ -670,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, @@ -697,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, @@ -717,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, @@ -739,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, @@ -749,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); @@ -767,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/person.rs b/crates/db_schema/src/impls/person.rs index 0e6dc8556..a5f8ae1a0 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -279,7 +279,6 @@ mod tests { public_key: "pubkey".to_owned(), last_refreshed_at: inserted_person.published, inbox_url: inserted_person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index ab8bfdc29..774cc5007 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -144,7 +144,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?; @@ -156,7 +156,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 } @@ -543,7 +543,6 @@ mod tests { 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?; diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index cad3db2b6..5507423e1 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -80,7 +80,6 @@ impl Reportable for PostReport { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use super::*; @@ -94,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, @@ -109,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, @@ -120,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 9387c0e96..264175fe2 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -85,7 +85,6 @@ impl PrivateMessage { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::{ @@ -97,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, @@ -125,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, @@ -141,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()), @@ -160,8 +153,7 @@ mod tests { inserted_private_message.id, &private_message_update_form, ) - .await - .unwrap(); + .await?; let deleted_private_message = PrivateMessage::update( pool, @@ -171,8 +163,7 @@ mod tests { ..Default::default() }, ) - .await - .unwrap(); + .await?; let marked_read_private_message = PrivateMessage::update( pool, inserted_private_message.id, @@ -181,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 963ac63d7..dbadaaf95 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -251,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/schema.rs b/crates/db_schema/src/schema.rs index 9617d9954..544da607b 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; @@ -119,7 +123,6 @@ diesel::table! { comment_like (person_id, comment_id) { person_id -> Int4, comment_id -> Int4, - post_id -> Int4, score -> Int2, published -> Timestamptz, } @@ -185,8 +188,6 @@ diesel::table! { followers_url -> Nullable, #[max_length = 255] inbox_url -> Varchar, - #[max_length = 255] - shared_inbox_url -> Nullable, hidden -> Bool, posting_restricted_to_mods -> Bool, instance_id -> Int4, @@ -368,12 +369,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, @@ -398,6 +399,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, } } @@ -679,8 +684,6 @@ diesel::table! { deleted -> Bool, #[max_length = 255] inbox_url -> Varchar, - #[max_length = 255] - shared_inbox_url -> Nullable, matrix_user_id -> Nullable, bot_account -> Bool, ban_expires -> Nullable, @@ -991,7 +994,6 @@ diesel::joinable!(comment -> post (post_id)); diesel::joinable!(comment_aggregates -> comment (comment_id)); diesel::joinable!(comment_like -> comment (comment_id)); diesel::joinable!(comment_like -> person (person_id)); -diesel::joinable!(comment_like -> post (post_id)); diesel::joinable!(comment_reply -> comment (comment_id)); diesel::joinable!(comment_reply -> person (recipient_id)); diesel::joinable!(comment_report -> comment (comment_id)); diff --git a/crates/db_schema/src/source/comment.rs b/crates/db_schema/src/source/comment.rs index e7d031c68..1e5f043f1 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -102,7 +102,6 @@ pub struct CommentUpdateForm { pub struct CommentLike { pub person_id: PersonId, pub comment_id: CommentId, - pub post_id: PostId, // TODO this is redundant pub score: i16, pub published: DateTime, } @@ -113,7 +112,6 @@ pub struct CommentLike { pub struct CommentLikeForm { pub person_id: PersonId, pub comment_id: CommentId, - pub post_id: PostId, // TODO this is redundant pub score: i16, } diff --git a/crates/db_schema/src/source/community.rs b/crates/db_schema/src/source/community.rs index ced85bf4c..853667f7f 100644 --- a/crates/db_schema/src/source/community.rs +++ b/crates/db_schema/src/source/community.rs @@ -54,8 +54,6 @@ pub struct Community { #[cfg_attr(feature = "full", ts(skip))] #[serde(skip, default = "placeholder_apub_url")] pub inbox_url: DbUrl, - #[serde(skip)] - pub shared_inbox_url: Option, /// Whether the community is hidden. pub hidden: bool, /// Whether posting is restricted to mods only. @@ -107,8 +105,6 @@ pub struct CommunityInsertForm { #[new(default)] pub inbox_url: Option, #[new(default)] - pub shared_inbox_url: Option, - #[new(default)] pub moderators_url: Option, #[new(default)] pub featured_url: Option, @@ -140,7 +136,6 @@ pub struct CommunityUpdateForm { pub banner: Option>, pub followers_url: Option, pub inbox_url: Option, - pub shared_inbox_url: Option>, pub moderators_url: Option, pub featured_url: Option, pub hidden: Option, 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/person.rs b/crates/db_schema/src/source/person.rs index 332b46eb5..c3aeeb4d7 100644 --- a/crates/db_schema/src/source/person.rs +++ b/crates/db_schema/src/source/person.rs @@ -48,8 +48,6 @@ pub struct Person { #[cfg_attr(feature = "full", ts(skip))] #[serde(skip, default = "placeholder_apub_url")] pub inbox_url: DbUrl, - #[serde(skip)] - pub shared_inbox_url: Option, /// A matrix id, usually given an @person:matrix.org pub matrix_user_id: Option, /// Whether the person is a bot account. @@ -93,8 +91,6 @@ pub struct PersonInsertForm { #[new(default)] pub inbox_url: Option, #[new(default)] - pub shared_inbox_url: Option, - #[new(default)] pub matrix_user_id: Option, #[new(default)] pub bot_account: Option, @@ -119,7 +115,6 @@ pub struct PersonUpdateForm { pub banner: Option>, pub deleted: Option, pub inbox_url: Option, - pub shared_inbox_url: Option>, pub matrix_user_id: Option>, pub bot_account: Option, pub ban_expires: Option>>, diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 4955dabea..c65e121e2 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -259,7 +259,6 @@ impl CommentReportQuery { } #[cfg(test)] -#[expect(clippy::unwrap_used)] #[expect(clippy::indexing_slicing)] mod tests { @@ -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(), @@ -416,7 +402,6 @@ mod tests { last_refreshed_at: inserted_community.last_refreshed_at, followers_url: inserted_community.followers_url, inbox_url: inserted_community.inbox_url, - shared_inbox_url: inserted_community.shared_inbox_url, moderators_url: inserted_community.moderators_url, featured_url: inserted_community.featured_url, instance_id: inserted_instance.id, @@ -437,7 +422,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_jessica.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, @@ -460,7 +444,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_timmy.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, @@ -502,7 +485,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_sara.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, @@ -514,8 +496,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 +507,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 @@ -570,7 +546,6 @@ mod tests { private_key: inserted_timmy.private_key.clone(), public_key: inserted_timmy.public_key.clone(), last_refreshed_at: inserted_timmy.last_refreshed_at, - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, @@ -588,24 +563,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 626a56b16..ac0e03f65 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)), + ), + )); } } @@ -423,7 +421,6 @@ impl<'a> CommentQuery<'a> { #[cfg(test)] #[expect(clippy::indexing_slicing)] -#[expect(clippy::unwrap_used)] 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( @@ -606,7 +601,6 @@ mod tests { let comment_like_form = CommentLikeForm { comment_id: inserted_comment_0.id, - post_id: inserted_post.id, person_id: inserted_timmy_person.id, score: 1, }; @@ -655,8 +649,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 { @@ -706,7 +700,6 @@ mod tests { // Like a new comment let comment_like_form = CommentLikeForm { comment_id: data.inserted_comment_1.id, - post_id: data.inserted_post.id, person_id: data.timmy_local_user_view.person.id, score: 1, }; @@ -832,9 +825,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 +844,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) @@ -1058,7 +1049,6 @@ mod tests { banner: None, updated: None, inbox_url: data.timmy_local_user_view.person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: data.inserted_instance.id, @@ -1114,7 +1104,6 @@ mod tests { last_refreshed_at: data.inserted_community.last_refreshed_at, followers_url: data.inserted_community.followers_url.clone(), inbox_url: data.inserted_community.inbox_url.clone(), - shared_inbox_url: data.inserted_community.shared_inbox_url.clone(), moderators_url: data.inserted_community.moderators_url.clone(), featured_url: data.inserted_community.featured_url.clone(), visibility: CommunityVisibility::Public, diff --git a/crates/db_views/src/local_user_view.rs b/crates/db_views/src/local_user_view.rs index b8ae14d5e..8d55b96fe 100644 --- a/crates/db_views/src/local_user_view.rs +++ b/crates/db_views/src/local_user_view.rs @@ -5,6 +5,12 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{LocalUserId, OAuthProviderId, PersonId}, schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates}, + source::{ + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + }, + traits::Crud, utils::{ functions::{coalesce, lower}, DbConn, @@ -134,6 +140,31 @@ impl LocalUserView { pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result, Error> { queries().list(pool, ListMode::AdminsWithEmails).await } + + pub async fn create_test_user( + pool: &mut DbPool<'_>, + name: &str, + bio: &str, + admin: bool, + ) -> Result { + let instance_id = Instance::read_or_create(pool, "example.com".to_string()) + .await? + .id; + let person_form = PersonInsertForm { + display_name: Some(name.to_owned()), + bio: Some(bio.to_owned()), + ..PersonInsertForm::test_form(instance_id, name) + }; + let person = Person::create(pool, &person_form).await?; + + let user_form = match admin { + true => LocalUserInsertForm::test_form_admin(person.id), + false => LocalUserInsertForm::test_form(person.id), + }; + let local_user = LocalUser::create(pool, &user_form, vec![]).await?; + + LocalUserView::read(pool, local_user.id).await + } } impl FromRequest for LocalUserView { diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index fa9941990..82e4c5d5b 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -284,7 +284,6 @@ impl PostReportQuery { } #[cfg(test)] -#[expect(clippy::unwrap_used)] #[expect(clippy::indexing_slicing)] mod tests { @@ -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 c8066b072..81e22dc5b 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 { @@ -739,7 +722,6 @@ impl<'a> PostQuery<'a> { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::{ post_view::{PaginationCursorData, PostQuery, PostView}, @@ -755,6 +737,8 @@ mod tests { comment::{Comment, CommentInsertForm}, community::{ Community, + CommunityFollower, + CommunityFollowerForm, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -783,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, @@ -1305,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), @@ -1437,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<()> { @@ -1690,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 } @@ -1726,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 } @@ -1795,7 +1812,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: data.inserted_instance.id, @@ -1830,7 +1846,6 @@ mod tests { last_refreshed_at: inserted_community.last_refreshed_at, followers_url: inserted_community.followers_url.clone(), inbox_url: inserted_community.inbox_url.clone(), - shared_inbox_url: inserted_community.shared_inbox_url.clone(), moderators_url: inserted_community.moderators_url.clone(), featured_url: inserted_community.featured_url.clone(), visibility: CommunityVisibility::Public, diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index c4bbf89ac..56d0d6e7b 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -111,7 +111,6 @@ impl PrivateMessageReportQuery { } #[cfg(test)] -#[expect(clippy::unwrap_used)] #[expect(clippy::indexing_slicing)] mod tests { @@ -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 cfac287d1..0fbc0ee16 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -173,7 +173,6 @@ impl PrivateMessageQuery { } #[cfg(test)] -#[expect(clippy::unwrap_used)] #[expect(clippy::indexing_slicing)] mod tests { @@ -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 32760a891..a0a40789b 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -135,7 +135,6 @@ impl RegistrationApplicationQuery { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::registration_application_view::{ @@ -156,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 { @@ -195,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 { @@ -219,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(), @@ -273,7 +258,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_sara_person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, instance_id: inserted_instance.id, private_key: inserted_sara_person.private_key, @@ -291,8 +275,7 @@ mod tests { ..Default::default() } .list(pool) - .await - .unwrap(); + .await?; assert_eq!( apps, @@ -300,9 +283,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 @@ -311,9 +292,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 { @@ -321,13 +300,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 @@ -351,7 +327,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_timmy_person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, instance_id: inserted_instance.id, private_key: inserted_timmy_person.private_key, @@ -367,28 +342,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 c077a778d..0fd64deca 100644 --- a/crates/db_views/src/vote_view.rs +++ b/crates/db_views/src/vote_view.rs @@ -10,7 +10,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommentId, PostId}, - schema::{comment_like, community_person_ban, person, post, post_like}, + schema::{comment, comment_like, community_person_ban, person, post, post_like}, utils::{get_conn, limit_and_offset, DbPool}, }; @@ -59,7 +59,8 @@ impl VoteView { comment_like::table .inner_join(person::table) - .inner_join(post::table) + .inner_join(comment::table) + .inner_join(post::table.on(comment::post_id.eq(post::id))) // Join to community_person_ban to get creator_banned_from_community .left_join( community_person_ban::table.on( @@ -83,7 +84,6 @@ impl VoteView { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use crate::structs::VoteView; @@ -98,26 +98,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, @@ -125,21 +124,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 { @@ -147,7 +146,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 { @@ -155,7 +154,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 { @@ -170,32 +169,24 @@ 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 let timmy_comment_vote_form = CommentLikeForm { - post_id: inserted_post.id, comment_id: inserted_comment.id, 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 { - post_id: inserted_post.id, comment_id: inserted_comment.id, 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 { @@ -210,9 +201,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 @@ -221,36 +211,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/community_follower_view.rs b/crates/db_views_actor/src/community_follower_view.rs index 7b942e043..92889d12d 100644 --- a/crates/db_views_actor/src/community_follower_view.rs +++ b/crates/db_views_actor/src/community_follower_view.rs @@ -10,7 +10,7 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, schema::{community, community_follower, person}, - utils::{functions::coalesce, get_conn, DbPool}, + utils::{get_conn, DbPool}, }; impl CommunityFollowerView { @@ -37,10 +37,7 @@ impl CommunityFollowerView { // local-person+remote-community or remote-person+local-community .filter(not(person::local)) .filter(community_follower::published.gt(published_since.naive_utc())) - .select(( - community::id, - coalesce(person::shared_inbox_url, person::inbox_url), - )) + .select((community::id, person::inbox_url)) .distinct() // only need each community_id, inbox combination once .load::<(CommunityId, DbUrl)>(conn) .await @@ -54,7 +51,7 @@ impl CommunityFollowerView { .filter(community_follower::community_id.eq(community_id)) .filter(not(person::local)) .inner_join(person::table) - .select(coalesce(person::shared_inbox_url, person::inbox_url)) + .select(person::inbox_url) .distinct() .load::(conn) .await?; diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 804d67152..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)); @@ -148,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 { @@ -228,10 +239,36 @@ 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, @@ -248,10 +285,12 @@ impl<'a> CommunityQuery<'a> { } #[cfg(test)] -#[expect(clippy::unwrap_used)] 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}, @@ -264,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(), @@ -317,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/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 d57149913..1649e019f 100644 --- a/crates/federate/src/inboxes.rs +++ b/crates/federate/src/inboxes.rs @@ -222,7 +222,6 @@ impl CommunityInboxCollector { } #[cfg(test)] -#[expect(clippy::unwrap_used)] #[expect(clippy::indexing_slicing)] mod tests { use super::*; @@ -230,6 +229,7 @@ mod tests { 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/worker.rs b/crates/federate/src/worker.rs index 0d4ad04d5..b0254ba0b 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -449,7 +449,7 @@ mod test { protocol::context::WithContext, }; use actix_web::{dev::ServerHandle, web, App, HttpResponse, HttpServer}; - use lemmy_api_common::utils::{generate_inbox_url, generate_shared_inbox_url}; + use lemmy_api_common::utils::generate_inbox_url; use lemmy_db_schema::{ newtypes::DbUrl, source::{ @@ -491,8 +491,7 @@ mod test { let person_form = PersonInsertForm { actor_id: Some(actor_id.clone()), private_key: (Some(actor_keypair.private_key)), - inbox_url: Some(generate_inbox_url(&actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + inbox_url: Some(generate_inbox_url()?), ..PersonInsertForm::new("alice".to_string(), actor_keypair.public_key, instance.id) }; let person = Person::create(&mut context.pool(), &person_form).await?; diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index f0369f7e9..1c99e89c2 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -82,6 +82,10 @@ ts-rs = { workspace = true, optional = true } enum-map = { workspace = true, optional = true } cfg-if = "1" clearurls = { version = "0.0.4", features = ["linkify"] } +markdown-it-block-spoiler = "1.0.0" +markdown-it-sub = "1.0.0" +markdown-it-sup = "1.0.0" +markdown-it-ruby = "1.0.0" [dev-dependencies] reqwest = { workspace = true } 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/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index e93adfa83..01d379986 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -308,10 +308,10 @@ fn split_ipv6(ip: Ipv6Addr) -> ([u8; 6], u8, u8) { } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use super::{ActionType, BucketConfig, InstantSecs, RateLimitState, RateLimitedGroup}; + use crate::error::LemmyResult; use pretty_assertions::assert_eq; #[test] @@ -326,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, @@ -350,7 +350,7 @@ 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); @@ -407,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); } @@ -419,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)); @@ -429,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 79b528aff..a51b507ce 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -1,18 +1,19 @@ -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; static MARKDOWN_PARSER: LazyLock = LazyLock::new(|| { let mut parser = MarkdownIt::new(); markdown_it::plugins::cmark::add(&mut parser); markdown_it::plugins::extra::add(&mut parser); - spoiler_rule::add(&mut parser); + markdown_it_block_spoiler::add(&mut parser); + markdown_it_sub::add(&mut parser); + markdown_it_sup::add(&mut parser); + markdown_it_ruby::add(&mut parser); link_rule::add(&mut parser); parser @@ -35,70 +36,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,10 +44,10 @@ pub fn markdown_check_for_blocked_urls(text: &str, blocklist: &RegexSet) -> Lemm } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use super::*; + use image_links::markdown_rewrite_image_links; use pretty_assertions::assert_eq; #[test] @@ -167,12 +104,22 @@ mod tests { ( "basic spoiler", "::: spoiler click to see more\nhow spicy!\n:::\n", - "
click to see more

how spicy!\n

\n" + "
click to see morehow spicy!\n
\n" ), ( "escape html special chars", " hello &\"", "

<script>alert(‘xss’);</script> hello &"

\n" + ),("subscript","log~2~(a)","

log2(a)

\n"), + ( + "superscript", + "Markdown^TM^", + "

MarkdownTM

\n" + ), + ( + "ruby text", + "{漢|Kan}{字|ji}", + "

(Kan)(ji)

\n" ) ]; @@ -245,8 +192,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() @@ -274,7 +221,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 @@ -285,8 +232,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() @@ -296,15 +242,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 deleted file mode 100644 index fd6450b31..000000000 --- a/crates/utils/src/utils/markdown/spoiler_rule.rs +++ /dev/null @@ -1,202 +0,0 @@ -// Custom Markdown plugin to manage spoilers. -// -// Matches the capability described in Lemmy UI: -// https://github.com/LemmyNet/lemmy-ui/blob/main/src/shared/utils.ts#L159 -// that is based off of: -// https://github.com/markdown-it/markdown-it-container/tree/master#example -// -// FORMAT: -// Input Markdown: ::: spoiler VISIBLE_TEXT\nHIDDEN_SPOILER\n:::\n -// Output HTML:
VISIBLE_TEXT

nHIDDEN_SPOILER

-// -// Anatomy of a spoiler: -// keyword -// ^ -// ::: spoiler VISIBLE_HINT -// ^ ^ -// begin fence visible text -// -// HIDDEN_SPOILER -// ^ -// hidden text -// -// ::: -// ^ -// end fence - -use markdown_it::{ - parser::{ - block::{BlockRule, BlockState}, - inline::InlineRoot, - }, - MarkdownIt, - Node, - NodeValue, - Renderer, -}; -use regex::Regex; -use std::sync::LazyLock; - -#[derive(Debug)] -struct SpoilerBlock { - visible_text: String, -} - -const SPOILER_PREFIX: &str = "::: spoiler "; -const SPOILER_SUFFIX: &str = ":::"; -const SPOILER_SUFFIX_NEWLINE: &str = ":::\n"; - -static SPOILER_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^::: spoiler .*$").expect("compile spoiler markdown regex.")); - -impl NodeValue for SpoilerBlock { - // Formats any node marked as a 'SpoilerBlock' into HTML. - // See the SpoilerBlockScanner#run implementation to see how these nodes get added to the tree. - fn render(&self, node: &Node, fmt: &mut dyn Renderer) { - fmt.cr(); - fmt.open("details", &node.attrs); - fmt.open("summary", &[]); - // Not allowing special styling to the visible text to keep it simple. - // If allowed, would need to parse the child nodes to assign to visible vs hidden text sections. - fmt.text(&self.visible_text); - fmt.close("summary"); - fmt.open("p", &[]); - fmt.contents(&node.children); - fmt.close("p"); - fmt.close("details"); - fmt.cr(); - } -} - -struct SpoilerBlockScanner; - -impl BlockRule for SpoilerBlockScanner { - // Invoked on every line in the provided Markdown text to check if the BlockRule applies. - // - // NOTE: This does NOT support nested spoilers at this time. - fn run(state: &mut BlockState) -> Option<(Node, usize)> { - let first_line: &str = state.get_line(state.line).trim(); - - // 1. Check if the first line contains the spoiler syntax... - if !SPOILER_REGEX.is_match(first_line) { - return None; - } - - let begin_spoiler_line_idx: usize = state.line + 1; - let mut end_fence_line_idx: usize = begin_spoiler_line_idx; - let mut has_end_fence: bool = false; - - // 2. Search for the end of the spoiler and find the index of the last line of the spoiler. - // There could potentially be multiple lines between the beginning and end of the block. - // - // Block ends with a line with ':::' or ':::\n'; it must be isolated from other markdown. - while end_fence_line_idx < state.line_max && !has_end_fence { - let next_line: &str = state.get_line(end_fence_line_idx).trim(); - - if next_line.eq(SPOILER_SUFFIX) || next_line.eq(SPOILER_SUFFIX_NEWLINE) { - has_end_fence = true; - break; - } - - end_fence_line_idx += 1; - } - - // 3. If available, construct and return the spoiler node to add to the tree. - if has_end_fence { - let (spoiler_content, mapping) = state.get_lines( - begin_spoiler_line_idx, - end_fence_line_idx, - state.blk_indent, - true, - ); - - let mut node = Node::new(SpoilerBlock { - visible_text: String::from(first_line.replace(SPOILER_PREFIX, "").trim()), - }); - - // Add the spoiler content as children; marking as a child tells the tree to process the - // node again, which means other Markdown syntax (ex: emphasis, links) can be rendered. - node - .children - .push(Node::new(InlineRoot::new(spoiler_content, mapping))); - - // NOTE: Not using begin_spoiler_line_idx here because of incorrect results when - // state.line == 0 (subtracts an idx) vs the expected correct result (adds an idx). - Some((node, end_fence_line_idx - state.line + 1)) - } else { - None - } - } -} - -pub fn add(markdown_parser: &mut MarkdownIt) { - markdown_parser.block.add_rule::(); -} - -#[cfg(test)] -mod tests { - - use crate::utils::markdown::spoiler_rule::add; - use markdown_it::MarkdownIt; - use pretty_assertions::assert_eq; - - #[test] - fn test_spoiler_markdown() { - let tests: Vec<_> = vec![ - ( - "invalid spoiler", - "::: spoiler click to see more\nbut I never finished", - "

::: spoiler click to see more\nbut I never finished

\n", - ), - ( - "another invalid spoiler", - "::: spoiler\nnever added the lead in\n:::", - "

::: spoiler\nnever added the lead in\n:::

\n", - ), - ( - "basic spoiler, but no newline at the end", - "::: spoiler click to see more\nhow spicy!\n:::", - "
click to see more

how spicy!\n

\n" - ), - ( - "basic spoiler with a newline at the end", - "::: spoiler click to see more\nhow spicy!\n:::\n", - "
click to see more

how spicy!\n

\n" - ), - ( - "spoiler with extra markdown on the call to action (no extra parsing)", - "::: spoiler _click to see more_\nhow spicy!\n:::\n", - "
_click to see more_

how spicy!\n

\n" - ), - ( - "spoiler with extra markdown in the fenced spoiler block", - "::: spoiler click to see more\n**how spicy!**\n*i have many lines*\n:::\n", - "
click to see more

how spicy!\ni have many lines\n

\n" - ), - ( - "spoiler mixed with other content", - "hey you\npsst, wanna hear a secret?\n::: spoiler lean in and i'll tell you\n**you are breathtaking!**\n:::\nwhatcha think about that?", - "

hey you\npsst, wanna hear a secret?

\n
lean in and i'll tell you

you are breathtaking!\n

\n

whatcha think about that?

\n" - ), - ( - "spoiler mixed with indented content", - "- did you know that\n::: spoiler the call was\n***coming from inside the house!***\n:::\n - crazy, right?", - "
    \n
  • did you know that
  • \n
\n
the call was

coming from inside the house!\n

\n
    \n
  • crazy, right?
  • \n
\n" - ) - ]; - - tests.iter().for_each(|&(msg, input, expected)| { - let md = &mut MarkdownIt::new(); - markdown_it::plugins::cmark::add(md); - add(md); - - assert_eq!( - md.parse(input).xrender(), - expected, - "Testing {}, with original input '{}'", - msg, - input - ); - }); - } -} diff --git a/crates/utils/src/utils/slurs.rs b/crates/utils/src/utils/slurs.rs index 0ce5b825e..2350822eb 100644 --- a/crates/utils/src/utils/slurs.rs +++ b/crates/utils/src/utils/slurs.rs @@ -61,16 +61,18 @@ pub(crate) fn slurs_vec_to_str(slurs: &[&str]) -> String { } #[cfg(test)] -#[expect(clippy::unwrap_used)] 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"; @@ -95,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/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-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-10-18-074533_no-individual-inboxes/down.sql b/migrations/2024-10-18-074533_no-individual-inboxes/down.sql new file mode 100644 index 000000000..6da025919 --- /dev/null +++ b/migrations/2024-10-18-074533_no-individual-inboxes/down.sql @@ -0,0 +1,6 @@ +ALTER TABLE person + ADD COLUMN shared_inbox_url varchar(255); + +ALTER TABLE community + ADD COLUMN shared_inbox_url varchar(255); + diff --git a/migrations/2024-10-18-074533_no-individual-inboxes/up.sql b/migrations/2024-10-18-074533_no-individual-inboxes/up.sql new file mode 100644 index 000000000..98cefcee0 --- /dev/null +++ b/migrations/2024-10-18-074533_no-individual-inboxes/up.sql @@ -0,0 +1,33 @@ +-- replace value of inbox_url with shared_inbox_url and the drop shared inbox +UPDATE + person +SET + inbox_url = subquery.inbox_url +FROM ( + SELECT + id, + coalesce(shared_inbox_url, inbox_url) AS inbox_url + FROM + person) AS subquery +WHERE + person.id = subquery.id; + +ALTER TABLE person + DROP COLUMN shared_inbox_url; + +UPDATE + community +SET + inbox_url = subquery.inbox_url +FROM ( + SELECT + id, + coalesce(shared_inbox_url, inbox_url) AS inbox_url + FROM + community) AS subquery +WHERE + community.id = subquery.id; + +ALTER TABLE community + DROP COLUMN shared_inbox_url; + diff --git a/migrations/2024-10-23-091053_comment-vote-remote-postid/down.sql b/migrations/2024-10-23-091053_comment-vote-remote-postid/down.sql new file mode 100644 index 000000000..21c730af3 --- /dev/null +++ b/migrations/2024-10-23-091053_comment-vote-remote-postid/down.sql @@ -0,0 +1,15 @@ +ALTER TABLE comment_like + ADD COLUMN post_id int; + +UPDATE + comment_like +SET + post_id = comment.post_id +FROM + comment +WHERE + comment_id = comment.id; + +ALTER TABLE comment_like + ALTER COLUMN post_id SET NOT NULL; + diff --git a/migrations/2024-10-23-091053_comment-vote-remote-postid/up.sql b/migrations/2024-10-23-091053_comment-vote-remote-postid/up.sql new file mode 100644 index 000000000..f6cbf3639 --- /dev/null +++ b/migrations/2024-10-23-091053_comment-vote-remote-postid/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE comment_like + DROP post_id; + 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/code_migrations.rs b/src/code_migrations.rs index e78f8bf0b..84af43ea7 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -10,13 +10,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_api_common::{ lemmy_db_views::structs::SiteView, - utils::{ - generate_followers_url, - generate_inbox_url, - generate_local_apub_endpoint, - generate_shared_inbox_url, - EndpointType, - }, + utils::{generate_followers_url, generate_inbox_url, generate_local_apub_endpoint, EndpointType}, }; use lemmy_db_schema::{ source::{ @@ -49,8 +43,8 @@ pub async fn run_advanced_migrations( comment_updates_2020_04_03(pool, protocol_and_hostname).await?; private_message_updates_2020_05_05(pool, protocol_and_hostname).await?; post_thumbnail_url_updates_2020_07_27(pool, protocol_and_hostname).await?; - apub_columns_2021_02_02(pool, settings).await?; - instance_actor_2022_01_28(pool, protocol_and_hostname, settings).await?; + apub_columns_2021_02_02(pool).await?; + instance_actor_2022_01_28(pool, protocol_and_hostname).await?; regenerate_public_keys_2022_07_05(pool).await?; initialize_local_site_2022_10_10(pool, settings).await?; @@ -282,36 +276,27 @@ async fn post_thumbnail_url_updates_2020_07_27( /// We are setting inbox and follower URLs for local and remote actors alike, because for now /// all federated instances are also Lemmy and use the same URL scheme. -async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>, settings: &Settings) -> LemmyResult<()> { +async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; info!("Running apub_columns_2021_02_02"); { - use lemmy_db_schema::schema::person::dsl::{inbox_url, person, shared_inbox_url}; + use lemmy_db_schema::schema::person::dsl::{inbox_url, person}; let persons = person .filter(inbox_url.like("http://changeme%")) .load::(conn) .await?; for p in &persons { - let inbox_url_ = generate_inbox_url(&p.actor_id)?; - let shared_inbox_url_ = generate_shared_inbox_url(settings)?; + let inbox_url_ = generate_inbox_url()?; diesel::update(person.find(p.id)) - .set(( - inbox_url.eq(inbox_url_), - shared_inbox_url.eq(shared_inbox_url_), - )) + .set((inbox_url.eq(inbox_url_),)) .get_result::(conn) .await?; } } { - use lemmy_db_schema::schema::community::dsl::{ - community, - followers_url, - inbox_url, - shared_inbox_url, - }; + use lemmy_db_schema::schema::community::dsl::{community, followers_url, inbox_url}; let communities = community .filter(inbox_url.like("http://changeme%")) .load::(conn) @@ -319,14 +304,9 @@ async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>, settings: &Settings) -> for c in &communities { let followers_url_ = generate_followers_url(&c.actor_id)?; - let inbox_url_ = generate_inbox_url(&c.actor_id)?; - let shared_inbox_url_ = generate_shared_inbox_url(settings)?; + let inbox_url_ = generate_inbox_url()?; diesel::update(community.find(c.id)) - .set(( - followers_url.eq(followers_url_), - inbox_url.eq(inbox_url_), - shared_inbox_url.eq(shared_inbox_url_), - )) + .set((followers_url.eq(followers_url_), inbox_url.eq(inbox_url_))) .get_result::(conn) .await?; } @@ -342,7 +322,6 @@ async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>, settings: &Settings) -> async fn instance_actor_2022_01_28( pool: &mut DbPool<'_>, protocol_and_hostname: &str, - settings: &Settings, ) -> LemmyResult<()> { info!("Running instance_actor_2021_09_29"); if let Ok(site_view) = SiteView::read_local(pool).await { @@ -356,7 +335,7 @@ async fn instance_actor_2022_01_28( let site_form = SiteUpdateForm { actor_id: Some(actor_id.clone().into()), last_refreshed_at: Some(naive_now()), - inbox_url: Some(generate_shared_inbox_url(settings)?), + inbox_url: Some(generate_inbox_url()?), private_key: Some(Some(key_pair.private_key)), public_key: Some(key_pair.public_key), ..Default::default() @@ -457,8 +436,7 @@ async fn initialize_local_site_2022_10_10( // Register the user if there's a site setup let person_form = PersonInsertForm { actor_id: Some(person_actor_id.clone()), - inbox_url: Some(generate_inbox_url(&person_actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(settings)?), + inbox_url: Some(generate_inbox_url()?), private_key: Some(person_keypair.private_key), ..PersonInsertForm::new( setup.admin_username.clone(), @@ -488,7 +466,7 @@ async fn initialize_local_site_2022_10_10( let site_form = SiteInsertForm { actor_id: Some(site_actor_id.clone().into()), last_refreshed_at: Some(naive_now()), - inbox_url: Some(generate_shared_inbox_url(settings)?), + inbox_url: Some(generate_inbox_url()?), private_key: Some(site_key_pair.private_key), public_key: Some(site_key_pair.public_key), diff --git a/src/lib.rs b/src/lib.rs index 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/session_middleware.rs b/src/session_middleware.rs index d2eedcb11..ec8f4399c 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -97,7 +97,6 @@ where } #[cfg(test)] -#[expect(clippy::unwrap_used)] mod tests { use super::*; @@ -113,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; @@ -122,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(), @@ -136,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(()) } }