diff --git a/.woodpecker.yml b/.woodpecker.yml index e832639e0..7e61e88f7 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -195,6 +195,7 @@ steps: LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy RUST_BACKTRACE: "1" CARGO_HOME: .cargo_home + LEMMY_TEST_FAST_FEDERATION: "1" commands: # Install pg_dump for the schema setup test (must match server version) - apt update && apt install -y lsb-release @@ -288,7 +289,7 @@ steps: services: database: - image: postgres:16-alpine + image: pgautoupgrade/pgautoupgrade:16-alpine environment: POSTGRES_DB: lemmy POSTGRES_USER: postgres diff --git a/Cargo.lock b/Cargo.lock index 23ffd189a..db6469ac2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,9 +16,9 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772" [[package]] name = "activitypub_federation" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8ff2d0151ce9ac02eb29e4a58b41d28693f141f7963d4bfabd2f9d402ecec7" +checksum = "b86eea7a032da501fe07b04a83c10716ea732c45e6943d7f045bc053aca03f2a" dependencies = [ "activitystreams-kinds", "actix-web", @@ -35,14 +35,15 @@ dependencies = [ "http-signature-normalization", "http-signature-normalization-reqwest", "httpdate", - "itertools 0.12.1", + "itertools 0.13.0", "moka", "once_cell", - "openssl", "pin-project-lite", + "rand", "regex", "reqwest 0.11.27", "reqwest-middleware 0.2.5", + "rsa", "serde", "serde_json", "sha2", @@ -68,7 +69,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytes", "futures-core", "futures-sink", @@ -96,9 +97,9 @@ dependencies = [ [[package]] name = "actix-form-data" -version = "0.7.0-beta.7" +version = "0.7.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2355a841a5d9a6c616d6a4f31336064116d206e6c1830de22730f983613a05" +checksum = "6a4f1e31610d53f56cb38c07fd8e10033e8462091528f3af6970f81a27ef9bba" dependencies = [ "actix-multipart", "actix-web", @@ -112,9 +113,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb9843d84c775696c37d9a418bbb01b932629d01870722c0f13eb3f95e2536d" +checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" dependencies = [ "actix-codec", "actix-rt", @@ -123,7 +124,7 @@ dependencies = [ "actix-utils", "ahash", "base64 0.22.1", - "bitflags 2.5.0", + "bitflags 2.6.0", "brotli", "bytes", "bytestring", @@ -157,18 +158,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] name = "actix-multipart" -version = "0.6.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" dependencies = [ "actix-utils", "actix-web", - "bytes", "derive_more", "futures-core", "futures-util", @@ -177,6 +177,7 @@ dependencies = [ "log", "memchr", "mime", + "rand", "serde", "serde_json", "serde_plain", @@ -199,9 +200,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" dependencies = [ "futures-core", "tokio", @@ -209,9 +210,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" +checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" dependencies = [ "actix-rt", "actix-service", @@ -266,9 +267,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.7.0" +version = "4.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6316df3fa569627c98b12557a8b6ff0674e5be4bb9b5e4ae2550ddb4964ed6" +checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" dependencies = [ "actix-codec", "actix-http", @@ -314,7 +315,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -496,9 +497,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" dependencies = [ "flate2", "futures-core", @@ -537,18 +538,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -564,12 +565,6 @@ dependencies = [ "quick-xml 0.31.0", ] -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -584,9 +579,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.7.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474d7cec9d0a1126fad1b224b767fcbf351c23b0309bb21ec210bcfd379926a5" +checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -597,9 +592,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.17.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7505fc3cb7acbf42699a43a79dd9caa4ed9e99861dfbb837c5c0fb5a0a8d2980" +checksum = "2e89b6941c2d1a7045538884d6e760ccfffdf8e1ffc2613d8efa74305e1f3752" dependencies = [ "bindgen", "cc", @@ -623,7 +618,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "itoa", "matchit 0.7.3", "memchr", @@ -632,7 +627,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", @@ -708,12 +703,11 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bb8" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7c2093d15d6a1d33b1f972e1c5ea3177748742b97a5f392aa83a65262c6780" +checksum = "b10cf871f3ff2ce56432fddc2615ac7acc3aa22ca321f8fea800846fbb32f188" dependencies = [ "async-trait", - "futures-channel", "futures-util", "parking_lot 0.12.3", "tokio", @@ -757,7 +751,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -770,7 +764,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.66", + "syn 2.0.72", "which", ] @@ -797,9 +791,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -858,9 +852,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -870,9 +864,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "bytestring" @@ -899,13 +893,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" dependencies = [ "jobserver", "libc", - "once_cell", ] [[package]] @@ -941,7 +934,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -977,9 +970,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -987,9 +980,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -1006,7 +999,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1103,7 +1096,7 @@ dependencies = [ "ron", "serde", "serde_json", - "toml 0.8.14", + "toml 0.8.15", "yaml-rust", ] @@ -1311,12 +1304,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core 0.20.9", - "darling_macro 0.20.9", + "darling_core 0.20.10", + "darling_macro 0.20.10", ] [[package]] @@ -1335,16 +1328,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1360,13 +1353,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core 0.20.9", + "darling_core 0.20.10", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1430,13 +1423,13 @@ dependencies = [ [[package]] name = "der_derive" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1468,7 +1461,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1486,10 +1479,10 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" dependencies = [ - "darling 0.20.9", + "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1499,20 +1492,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn 2.0.72", ] [[package]] @@ -1530,7 +1523,7 @@ version = "2.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "chrono", "diesel_derives", @@ -1566,7 +1559,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1577,7 +1570,7 @@ checksum = "d5adf688c584fe33726ce0e2898f608a2a92578ac94a4a92fcecf73214fe0716" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1589,7 +1582,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1619,7 +1612,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1635,6 +1628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1648,17 +1642,6 @@ dependencies = [ "chrono", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "doku" version = "0.21.1" @@ -1683,6 +1666,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1703,9 +1692,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elementtree" @@ -1728,9 +1717,9 @@ dependencies = [ [[package]] name = "email_address" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" +checksum = "46b7a0ac6570e31bfe2c6cf575a576a55af9893d1a6b30b4444e6e90b216bb84" [[package]] name = "encoding_rs" @@ -1764,7 +1753,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -1880,9 +1869,9 @@ dependencies = [ [[package]] name = "flagset" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" [[package]] name = "flate2" @@ -1924,6 +1913,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "fs2" version = "0.4.3" @@ -2006,7 +2001,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -2201,17 +2196,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "hostname" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" -dependencies = [ - "cfg-if", - "libc", - "windows", -] - [[package]] name = "hound" version = "3.5.1" @@ -2279,7 +2263,7 @@ dependencies = [ "markup5ever 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -2317,9 +2301,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -2327,14 +2311,14 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -2366,9 +2350,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -2384,9 +2368,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -2408,16 +2392,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", "h2 0.4.5", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -2429,19 +2413,34 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.26.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", - "rustls 0.22.4", + "rustls 0.23.11", "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tower-service", + "webpki-roots 0.26.3", ] [[package]] @@ -2450,7 +2449,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.29", + "hyper 0.14.30", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2463,7 +2462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.30", "native-tls", "tokio", "tokio-native-tls", @@ -2471,16 +2470,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -2506,7 +2505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8215279f83f9b829403812f845aa2d0dd5966332aa2fd0334a375256f3dd0322" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -2532,124 +2531,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2676,18 +2557,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "idna" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" -dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", -] - [[package]] name = "image" version = "0.24.9" @@ -2859,9 +2728,12 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "lazycell" @@ -2990,7 +2862,7 @@ dependencies = [ "serde_with", "serial_test", "stringreader", - "strum_macros", + "strum", "tokio", "tracing", "url", @@ -3037,13 +2909,12 @@ dependencies = [ "once_cell", "pretty_assertions", "regex", - "rustls 0.23.10", + "rustls 0.23.11", "serde", "serde_json", "serde_with", "serial_test", "strum", - "strum_macros", "tokio", "tokio-postgres", "tokio-postgres-rustls", @@ -3091,7 +2962,6 @@ dependencies = [ "serde_with", "serial_test", "strum", - "strum_macros", "tokio", "ts-rs", "url", @@ -3114,7 +2984,9 @@ name = "lemmy_federate" version = "0.19.5" dependencies = [ "activitypub_federation", + "actix-web", "anyhow", + "async-trait", "chrono", "diesel", "diesel-async", @@ -3124,14 +2996,19 @@ dependencies = [ "lemmy_db_schema", "lemmy_db_views_actor", "lemmy_utils", + "mockall", "moka", "once_cell", "reqwest 0.11.27", "serde_json", "serial_test", + "test-context", "tokio", "tokio-util", "tracing", + "tracing-test", + "url", + "uuid", ] [[package]] @@ -3190,6 +3067,7 @@ dependencies = [ "reqwest 0.11.27", "reqwest-middleware 0.2.5", "reqwest-tracing 0.4.8", + "rustls 0.23.11", "serde_json", "serial_test", "tokio", @@ -3220,7 +3098,6 @@ dependencies = [ "lettre", "markdown-it", "once_cell", - "openssl", "pretty_assertions", "regex", "reqwest 0.11.27", @@ -3231,7 +3108,6 @@ dependencies = [ "serde_json", "smart-default", "strum", - "strum_macros", "tokio", "tracing", "tracing-error", @@ -3255,18 +3131,19 @@ dependencies = [ "fastrand", "futures-io", "futures-util", - "hostname", "httpdate", "idna 0.5.0", "mime", - "native-tls", "nom", "percent-encoding", "quoted_printable", + "rustls 0.23.11", + "rustls-pemfile 2.1.2", "socket2", "tokio", - "tokio-native-tls", + "tokio-rustls 0.26.0", "url", + "webpki-roots 0.26.3", ] [[package]] @@ -3277,19 +3154,19 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] -name = "line-wrap" -version = "0.2.0" +name = "libm" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linked-hash-map" @@ -3312,12 +3189,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "local-channel" version = "0.1.5" @@ -3347,9 +3218,9 @@ dependencies = [ [[package]] name = "lodepng" -version = "3.10.1" +version = "3.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42d298694b14401847de29abd44adf278b42e989e516deac7b72018400002d8" +checksum = "72decff904ebe3e39c88b0488985f24e9796bb4dc5bcf65beebaa5d3b7073ff4" dependencies = [ "crc32fast", "fallible_collections", @@ -3360,9 +3231,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "mac" @@ -3372,9 +3243,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "markdown-it" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "181d1b4704b0a2fc6027d3f758a7eedb975dcbaab86e6794901b8cfc52af94d5" +checksum = "f99c010929c8217b2dc0940954267a2e15a15f17cb309cd1f299e21933f84fac" dependencies = [ "argparse", "const_format", @@ -3462,9 +3333,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "matchit" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "540f1c43aed89909c0cc0cc604e3bb2f7e7a341a3728a9e6cfe760e733cd11ed" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "md-5" @@ -3489,9 +3360,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "metrics" @@ -3505,13 +3376,13 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.15.0" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26eb45aff37b45cff885538e1dcbd6c2b462c04fe84ce0155ea469f325672c98" +checksum = "b4f0c8427b39666bf970460908b213ec09b3b350f20c0c2eabcbba51704a08e6" dependencies = [ "base64 0.22.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "indexmap 2.2.6", "ipnet", @@ -3567,9 +3438,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -3583,9 +3454,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", @@ -3610,10 +3481,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" [[package]] -name = "moka" -version = "0.12.7" +name = "mockall" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e0d88686dc561d743b40de8269b26eaf0dc58781bde087b0984646602021d08" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" dependencies = [ "async-lock", "async-trait", @@ -3696,14 +3594,31 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3719,6 +3634,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3726,6 +3652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3755,11 +3682,11 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -3776,7 +3703,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -3787,9 +3714,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -3961,9 +3888,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be" dependencies = [ "num-traits", ] @@ -4029,9 +3956,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.3", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4179,10 +4106,10 @@ dependencies = [ "opentelemetry_sdk 0.23.0", "pin-project-lite", "refinery", - "reqwest 0.12.4", - "reqwest-middleware 0.3.1", - "reqwest-tracing 0.5.0", - "rustls 0.23.10", + "reqwest 0.12.5", + "reqwest-middleware 0.3.2", + "reqwest-tracing 0.5.2", + "rustls 0.23.11", "rustls-channel-resolver", "rustls-pemfile 2.1.2", "rusty-s3", @@ -4200,7 +4127,7 @@ dependencies = [ "tokio-postgres", "tokio-postgres-generic-rustls", "tokio-util", - "toml 0.8.14", + "toml 0.8.15", "tracing", "tracing-actix-web", "tracing-error", @@ -4209,7 +4136,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", - "webpki-roots", + "webpki-roots 0.26.3", ] [[package]] @@ -4229,7 +4156,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -4244,6 +4171,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -4252,14 +4200,13 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plist" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "indexmap 2.2.6", - "line-wrap", - "quick-xml 0.31.0", + "quick-xml 0.32.0", "serde", "time", ] @@ -4279,15 +4226,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "postgres" -version = "0.19.7" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7915b33ed60abc46040cbcaa25ffa1c7ec240668e0477c4f3070786f5916d451" +checksum = "6c9ec84ab55b0f9e418675de50052d494ba893fd28c65769a6e68fcdacbee2b8" dependencies = [ "bytes", "fallible-iterator", @@ -4299,11 +4246,11 @@ dependencies = [ [[package]] name = "postgres-protocol" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" +checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "byteorder", "bytes", "fallible-iterator", @@ -4317,9 +4264,9 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +checksum = "02048d9e032fb3cc3413bbf7b83a15d84a5d419778e2628751896d856498eee9" dependencies = [ "bytes", "fallible-iterator", @@ -4357,6 +4304,32 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -4374,14 +4347,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -4392,7 +4365,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "hex", "lazy_static", "procfs-core", @@ -4405,7 +4378,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "hex", ] @@ -4469,7 +4442,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -4531,6 +4504,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.11", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls 0.23.11", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a78e6f726d84fcf960409f509ae354a32648f090c8d32a2ea8b1a1bc3bab14" +dependencies = [ + "libc", + "once_cell", + "socket2", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -4542,9 +4570,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "rand" @@ -4578,11 +4606,11 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.0.2" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd" +checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -4593,7 +4621,7 @@ checksum = "a25d631e41bfb5fdcde1d4e2215f62f7f0afa3ff11e26563765bd6ea1d229aeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -4616,11 +4644,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -4650,7 +4678,7 @@ dependencies = [ "time", "tokio", "tokio-postgres", - "toml 0.8.14", + "toml 0.8.15", "url", "walkdir", ] @@ -4666,7 +4694,7 @@ dependencies = [ "quote", "refinery-core", "regex", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -4677,8 +4705,8 @@ checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -4692,20 +4720,20 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] name = "regex-lite" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" @@ -4715,9 +4743,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" @@ -4734,7 +4762,8 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", + "hyper-rustls 0.24.2", "hyper-tls", "ipnet", "js-sys", @@ -4745,14 +4774,16 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", @@ -4760,24 +4791,25 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots 0.25.4", "winreg 0.50.0", ] [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", "futures-core", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", - "hyper-rustls", + "hyper 1.4.1", + "hyper-rustls 0.27.2", "hyper-util", "ipnet", "js-sys", @@ -4786,15 +4818,16 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.22.4", + "quinn", + "rustls 0.23.11", "rustls-pemfile 2.1.2", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tokio-util", "tower-service", "url", @@ -4802,7 +4835,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 0.26.3", "winreg 0.52.0", ] @@ -4823,14 +4856,14 @@ dependencies = [ [[package]] name = "reqwest-middleware" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45d100244a467870f6cb763c4484d010a6bed6bd610b3676e3825d93fb4cfbd" +checksum = "39346a33ddfe6be00cbc17a34ce996818b97b230b87229f10114693becca1268" dependencies = [ "anyhow", "async-trait", "http 1.1.0", - "reqwest 0.12.4", + "reqwest 0.12.5", "serde", "thiserror", "tower-service", @@ -4856,17 +4889,17 @@ dependencies = [ [[package]] name = "reqwest-tracing" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b253954a1979e02eabccd7e9c3d61d8f86576108baa160775e7f160bb4e800a3" +checksum = "4e45dcad05dc210fdb0278d62a679eb768730af808f8cb552f810da89bdbe76d" dependencies = [ "anyhow", "async-trait", "getrandom", "http 1.1.0", - "matchit 0.8.2", - "reqwest 0.12.4", - "reqwest-middleware 0.3.1", + "matchit 0.8.4", + "reqwest 0.12.5", + "reqwest-middleware 0.3.2", "tracing", ] @@ -4878,9 +4911,9 @@ checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" [[package]] name = "rgb" -version = "0.8.37" +version = "0.8.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +checksum = "ade4539f42266ded9e755c605bdddf546242b2c961b03b06a7375260788a0523" dependencies = [ "bytemuck", ] @@ -4907,7 +4940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "serde", "serde_derive", ] @@ -4932,6 +4965,26 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f8c01b9158de3aa5a7ac041a41c0e854d7adc3e473e7d7e2143eb5432bc5ba2" +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rss" version = "2.0.8" @@ -4971,7 +5024,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -4980,30 +5033,28 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.4" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "rustls-webpki 0.101.7", + "sct", ] [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.6", "subtle", "zeroize", ] @@ -5015,7 +5066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fede2a247359da6b4998f7723ec6468c2d6a577a5d8c17e54f21806426ad2290" dependencies = [ "nanorand", - "rustls 0.23.10", + "rustls 0.23.11", ] [[package]] @@ -5045,9 +5096,19 @@ checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "aws-lc-rs", "ring", @@ -5097,9 +5158,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.1" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +checksum = "a4465c22496331e20eb047ff46e7366455bc01c0c02015c4a376de0b2cd3a1af" dependencies = [ "sdd", ] @@ -5130,18 +5191,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "sdd" -version = "0.2.0" +name = "sct" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "sdd" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f05a494052771fc5bd0619742363b5e24e5ad72ab3111ec2e27925b8edc5f3" [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -5150,9 +5221,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -5177,9 +5248,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] @@ -5195,13 +5266,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -5248,9 +5319,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64 0.22.1", "chrono", @@ -5266,14 +5337,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ - "darling 0.20.9", + "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -5298,7 +5369,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -5353,6 +5424,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core", ] @@ -5441,7 +5513,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -5470,12 +5542,6 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "stacker" version = "0.1.15" @@ -5562,9 +5628,12 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" @@ -5576,14 +5645,14 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -5598,9 +5667,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -5614,15 +5683,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "synstructure" -version = "0.13.1" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "syntect" @@ -5637,7 +5701,7 @@ dependencies = [ "fnv", "once_cell", "plist", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", "serde", "serde_derive", "serde_json", @@ -5715,23 +5779,50 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.61" +name = "termtree" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "test-context" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6676ab8513edfd2601a108621103fdb45cac9098305ca25ec93f7023b06b05d9" +dependencies = [ + "futures", + "test-context-macros", +] + +[[package]] +name = "test-context-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ea17a2dc368aeca6f554343ced1b1e31f76d63683fa8016e5844bd7a5144a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -5781,21 +5872,11 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -5824,14 +5905,14 @@ checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] name = "tokio" -version = "1.38.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -5865,7 +5946,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -5880,9 +5961,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" +checksum = "03adcf0147e203b6032c0b2d30be1415ba03bc348901f3ff1cc0df6a733e60c3" dependencies = [ "async-trait", "byteorder", @@ -5911,7 +5992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8e98c31c29b2666fb28720739e11476166be4ead1610a37dcd7414bb124413a" dependencies = [ "aws-lc-rs", - "rustls 0.23.10", + "rustls 0.23.11", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", @@ -5925,7 +6006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" dependencies = [ "ring", - "rustls 0.23.10", + "rustls 0.23.11", "tokio", "tokio-postgres", "tokio-rustls 0.26.0", @@ -5934,12 +6015,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", + "rustls 0.21.12", "tokio", ] @@ -5949,7 +6029,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", ] @@ -5992,14 +6072,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit 0.22.16", ] [[package]] @@ -6026,15 +6106,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow 0.6.14", ] [[package]] @@ -6053,7 +6133,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-timeout", "percent-encoding", "pin-project", @@ -6083,7 +6163,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-timeout", "percent-encoding", "pin-project", @@ -6110,7 +6190,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-timeout", "percent-encoding", "pin-project", @@ -6206,7 +6286,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -6338,10 +6418,31 @@ dependencies = [ ] [[package]] -name = "triomphe" -version = "0.1.12" +name = "tracing-test" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn 2.0.72", +] + +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" [[package]] name = "try-lock" @@ -6369,7 +6470,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", "termcolor", ] @@ -6390,7 +6491,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] @@ -6467,12 +6568,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna 1.0.0", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -6489,37 +6590,24 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ - "atomic", "getrandom", "serde", ] @@ -6594,7 +6682,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", "wasm-bindgen-shared", ] @@ -6628,7 +6716,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.72", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6702,9 +6790,15 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.2" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -6763,23 +6857,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core", - "windows-targets 0.52.5", -] - [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6797,7 +6881,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6817,18 +6901,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -6839,9 +6923,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -6851,9 +6935,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -6863,15 +6947,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -6881,9 +6965,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -6893,9 +6977,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -6905,9 +6989,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -6917,9 +7001,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -6932,9 +7016,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "374ec40a2d767a3c1b4972d9475ecd557356637be906f2cb3f7fe17a6eb5e22f" dependencies = [ "memchr", ] @@ -6959,18 +7043,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "x509-cert" version = "0.2.5" @@ -7045,69 +7117,24 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", -] - [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", + "syn 2.0.72", ] [[package]] @@ -7127,54 +7154,32 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "syn 2.0.72", ] [[package]] name = "zstd" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index ce6d3357d..96c4e2800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,7 @@ lemmy_db_views = { version = "=0.19.5", path = "./crates/db_views" } lemmy_db_views_actor = { version = "=0.19.5", path = "./crates/db_views_actor" } lemmy_db_views_moderator = { version = "=0.19.5", path = "./crates/db_views_moderator" } lemmy_federate = { version = "=0.19.5", path = "./crates/federate" } -activitypub_federation = { version = "0.5.6", default-features = false, features = [ +activitypub_federation = { version = "0.5.8", default-features = false, features = [ "actix-web", ] } diesel = "2.1.6" @@ -121,7 +121,12 @@ tracing-error = "0.2.0" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } url = { version = "2.5.0", features = ["serde"] } -reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] } +reqwest = { version = "0.11.27", default-features = false, features = [ + "json", + "blocking", + "gzip", + "rustls-tls", +] } reqwest-middleware = "0.2.5" reqwest-tracing = "0.4.8" clokwerk = "0.4.0" @@ -144,8 +149,7 @@ regex = "1.10.4" once_cell = "1.19.0" diesel-derive-newtype = "2.1.2" diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } -strum = "0.26.2" -strum_macros = "0.26.4" +strum = { version = "0.26.3", features = ["derive"] } itertools = "0.13.0" futures = "0.3.30" http = "0.2.12" @@ -198,6 +202,7 @@ opentelemetry = { workspace = true, optional = true } console-subscriber = { version = "0.3.0", optional = true } opentelemetry-otlp = { version = "0.12.0", optional = true } pict-rs = { version = "0.5.15", optional = true } +rustls = { workspace = true } tokio.workspace = true actix-cors = "0.7.0" futures-util = { workspace = true } diff --git a/api_tests/src/follow.spec.ts b/api_tests/src/follow.spec.ts index 161c7f045..22fdfa305 100644 --- a/api_tests/src/follow.spec.ts +++ b/api_tests/src/follow.spec.ts @@ -11,6 +11,7 @@ import { betaUrl, registerUser, unfollows, + delay, } from "./shared"; beforeAll(setupLogins); @@ -21,39 +22,48 @@ test("Follow local community", async () => { let user = await registerUser(beta, betaUrl); let community = (await resolveBetaCommunity(user)).community!; - expect(community.counts.subscribers).toBe(1); - expect(community.counts.subscribers_local).toBe(1); let follow = await followCommunity(user, true, community.community.id); // Make sure the follow response went through expect(follow.community_view.community.local).toBe(true); expect(follow.community_view.subscribed).toBe("Subscribed"); - expect(follow.community_view.counts.subscribers).toBe(2); - expect(follow.community_view.counts.subscribers_local).toBe(2); + expect(follow.community_view.counts.subscribers).toBe( + community.counts.subscribers + 1, + ); + expect(follow.community_view.counts.subscribers_local).toBe( + community.counts.subscribers_local + 1, + ); // Test an unfollow let unfollow = await followCommunity(user, false, community.community.id); expect(unfollow.community_view.subscribed).toBe("NotSubscribed"); - expect(unfollow.community_view.counts.subscribers).toBe(1); - expect(unfollow.community_view.counts.subscribers_local).toBe(1); + expect(unfollow.community_view.counts.subscribers).toBe( + community.counts.subscribers, + ); + expect(unfollow.community_view.counts.subscribers_local).toBe( + community.counts.subscribers_local, + ); }); test("Follow federated community", async () => { // It takes about 1 second for the community aggregates to federate - let betaCommunity = ( + await delay(2000); // if this is the second test run, we don't have a way to wait for the correct number of subscribers + const betaCommunityInitial = ( await waitUntil( () => resolveBetaCommunity(alpha), - c => - c.community?.counts.subscribers === 1 && - c.community.counts.subscribers_local === 0, + c => !!c.community && c.community?.counts.subscribers >= 1, ) ).community; - if (!betaCommunity) { + if (!betaCommunityInitial) { throw "Missing beta community"; } - let follow = await followCommunity(alpha, true, betaCommunity.community.id); + let follow = await followCommunity( + alpha, + true, + betaCommunityInitial.community.id, + ); expect(follow.community_view.subscribed).toBe("Pending"); - betaCommunity = ( + const betaCommunity = ( await waitUntil( () => resolveBetaCommunity(alpha), c => c.community?.subscribed === "Subscribed", @@ -64,20 +74,24 @@ test("Follow federated community", async () => { expect(betaCommunity?.community.local).toBe(false); expect(betaCommunity?.community.name).toBe("main"); expect(betaCommunity?.subscribed).toBe("Subscribed"); - expect(betaCommunity?.counts.subscribers_local).toBe(1); + expect(betaCommunity?.counts.subscribers_local).toBe( + betaCommunityInitial.counts.subscribers_local + 1, + ); // check that unfollow was federated let communityOnBeta1 = await resolveBetaCommunity(beta); - expect(communityOnBeta1.community?.counts.subscribers).toBe(2); - expect(communityOnBeta1.community?.counts.subscribers_local).toBe(1); + expect(communityOnBeta1.community?.counts.subscribers).toBe( + betaCommunityInitial.counts.subscribers + 1, + ); // Check it from local let site = await getSite(alpha); let remoteCommunityId = site.my_user?.follows.find( - c => c.community.local == false, + c => + c.community.local == false && + c.community.id === betaCommunityInitial.community.id, )?.community.id; expect(remoteCommunityId).toBeDefined(); - expect(site.my_user?.follows.length).toBe(2); if (!remoteCommunityId) { throw "Missing remote community id"; @@ -89,10 +103,21 @@ test("Follow federated community", async () => { // Make sure you are unsubbed locally let siteUnfollowCheck = await getSite(alpha); - expect(siteUnfollowCheck.my_user?.follows.length).toBe(1); + expect( + siteUnfollowCheck.my_user?.follows.find( + c => c.community.id === betaCommunityInitial.community.id, + ), + ).toBe(undefined); // check that unfollow was federated - let communityOnBeta2 = await resolveBetaCommunity(beta); - expect(communityOnBeta2.community?.counts.subscribers).toBe(1); + let communityOnBeta2 = await waitUntil( + () => resolveBetaCommunity(beta), + c => + c.community?.counts.subscribers === + betaCommunityInitial.counts.subscribers, + ); + expect(communityOnBeta2.community?.counts.subscribers).toBe( + betaCommunityInitial.counts.subscribers, + ); expect(communityOnBeta2.community?.counts.subscribers_local).toBe(1); }); diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index fe17bd979..6b5c8d812 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -52,17 +52,23 @@ beforeAll(async () => { afterAll(unfollows); -async function assertPostFederation(postOne: PostView, postTwo: PostView) { +async function assertPostFederation( + postOne: PostView, + postTwo: PostView, + waitForMeta = true, +) { // Link metadata is generated in background task and may not be ready yet at this time, // so wait for it explicitly. For removed posts we cant refetch anything. - postOne = await waitForPost(beta, postOne.post, res => { - return res === null || res?.post.embed_title !== null; - }); - postTwo = await waitForPost( - beta, - postTwo.post, - res => res === null || res?.post.embed_title !== null, - ); + if (waitForMeta) { + postOne = await waitForPost(beta, postOne.post, res => { + return res === null || !!res?.post.embed_title; + }); + postTwo = await waitForPost( + beta, + postTwo.post, + res => res === null || !!res?.post.embed_title, + ); + } expect(postOne?.post.ap_id).toBe(postTwo?.post.ap_id); expect(postOne?.post.name).toBe(postTwo?.post.name); @@ -408,7 +414,11 @@ test("Remove a post from admin and community on same instance", async () => { p => p?.post_view.post.removed ?? false, ); expect(alphaPost?.post_view.post.removed).toBe(true); - await assertPostFederation(alphaPost.post_view, removePostRes.post_view); + await assertPostFederation( + alphaPost.post_view, + removePostRes.post_view, + false, + ); // Undelete let undeletedPost = await removePost(beta, false, betaPost.post); diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index d008dcdc3..2edcf54ea 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -131,7 +131,11 @@ test("Requests with invalid auth should be treated as unauthenticated", async () }); test("Create user with Arabic name", async () => { - let user = await registerUser(alpha, alphaUrl, "تجريب"); + let user = await registerUser( + alpha, + alphaUrl, + "تجريب" + Math.random().toString().slice(2, 10), // less than actor_name_max_length + ); let site = await getSite(user); expect(site.my_user).toBeDefined(); diff --git a/config/defaults.hjson b/config/defaults.hjson index e8a70ebae..4bce48b5f 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -108,10 +108,12 @@ port: 8536 # Whether the site is available over TLS. Needs to be true for federation to work. tls_enabled: true - # The number of activitypub federation workers that can be in-flight concurrently - worker_count: 0 - # The number of activitypub federation retry workers that can be in-flight concurrently - retry_count: 0 + federation: { + # Limit to the number of concurrent outgoing federation requests per target instance. + # Set this to a higher value than 1 (e.g. 6) only if you have a huge instance (>10 activities + # per second) and if a receiving instance is not keeping up. + concurrent_sends_per_instance: 1 + } prometheus: { bind: "127.0.0.1" port: 10002 diff --git a/crates/api/src/comment/distinguish.rs b/crates/api/src/comment/distinguish.rs index dfd850e89..0683af9a4 100644 --- a/crates/api/src/comment/distinguish.rs +++ b/crates/api/src/comment/distinguish.rs @@ -17,9 +17,13 @@ pub async fn distinguish_comment( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + data.comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, @@ -54,7 +58,7 @@ pub async fn distinguish_comment( let comment_view = CommentView::read( &mut context.pool(), data.comment_id, - Some(local_user_view.person.id), + Some(&local_user_view.local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index d0aa4a6c2..b8a1c6f76 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -35,9 +35,13 @@ pub async fn like_comment( check_bot_account(&local_user_view.person)?; let comment_id = data.comment_id; - let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, diff --git a/crates/api/src/comment/list_comment_likes.rs b/crates/api/src/comment/list_comment_likes.rs index 8c2c9dd32..4b2e1c8b3 100644 --- a/crates/api/src/comment/list_comment_likes.rs +++ b/crates/api/src/comment/list_comment_likes.rs @@ -17,7 +17,7 @@ pub async fn list_comment_likes( let comment_view = CommentView::read( &mut context.pool(), data.comment_id, - Some(local_user_view.person.id), + Some(&local_user_view.local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs index f9d649e48..67c2db331 100644 --- a/crates/api/src/comment/save.rs +++ b/crates/api/src/comment/save.rs @@ -32,10 +32,13 @@ pub async fn save_comment( } let comment_id = data.comment_id; - let person_id = local_user_view.person.id; - let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; Ok(Json(CommentResponse { comment_view, diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs index c008d1df2..a269df07f 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/comment_report/create.rs @@ -35,9 +35,13 @@ pub async fn create_comment_report( let person_id = local_user_view.person.id; let comment_id = data.comment_id; - let comment_view = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 5245f592e..8d8826cd2 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, + local_user::LocalUser, moderator::{ModAddCommunity, ModAddCommunityForm}, }, traits::{Crud, Joinable}, @@ -33,6 +34,18 @@ pub async fn add_mod_to_community( &mut context.pool(), ) .await?; + + // If its a mod removal, also check that you're a higher mod. + if !data.added { + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + community_id, + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + } + let community = Community::read(&mut context.pool(), community_id) .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?; diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 877d9464f..8e527d2ac 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -14,6 +14,7 @@ use lemmy_db_schema::{ CommunityPersonBan, CommunityPersonBanForm, }, + local_user::LocalUser, moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, }, traits::{Bannable, Crud, Followable}, @@ -44,6 +45,14 @@ pub async fn ban_from_community( ) .await?; + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + data.community_id, + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + if let Some(reason) = &data.reason { is_valid_body_field(reason, false)?; } diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs index 449addf32..ad31548ea 100644 --- a/crates/api/src/community/block.rs +++ b/crates/api/src/community/block.rs @@ -50,10 +50,14 @@ pub async fn block_community( .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?; } - let community_view = - CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community_view = CommunityView::read( + &mut context.pool(), + community_id, + Some(&local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; ActivityChannel::submit_activity( SendActivityData::FollowCommunity( diff --git a/crates/api/src/community/follow.rs b/crates/api/src/community/follow.rs index 853cfde14..2236fa5bc 100644 --- a/crates/api/src/community/follow.rs +++ b/crates/api/src/community/follow.rs @@ -62,11 +62,14 @@ pub async fn follow_community( } let community_id = data.community_id; - let person_id = local_user_view.person.id; - let community_view = - CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community_view = CommunityView::read( + &mut context.pool(), + community_id, + Some(&local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index 5f3a6032e..a32c069b1 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -76,11 +76,14 @@ pub async fn transfer_community( ModTransferCommunity::create(&mut context.pool(), &form).await?; let community_id = data.community_id; - let person_id = local_user_view.person.id; - let community_view = - CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) - .await? - .ok_or(LemmyErrorType::CouldntFindCommunity)?; + let community_view = CommunityView::read( + &mut context.pool(), + community_id, + Some(&local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let community_id = data.community_id; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id) diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs index 7db2e5653..44b36fe66 100644 --- a/crates/api/src/local_user/add_admin.rs +++ b/crates/api/src/local_user/add_admin.rs @@ -24,6 +24,16 @@ pub async fn add_admin( // Make sure user is an admin is_admin(&local_user_view)?; + // If its an admin removal, also check that you're a higher admin + if !data.added { + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + } + // Make sure that the person_id added is local let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id) .await? diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 49cd6893a..58392cefd 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + local_user::LocalUser, login_token::LoginToken, moderator::{ModBan, ModBanForm}, person::{Person, PersonUpdateForm}, @@ -31,6 +32,14 @@ pub async fn ban_from_site( // Make sure user is an admin is_admin(&local_user_view)?; + // Also make sure you're a higher admin than the target + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + if let Some(reason) = &data.reason { is_valid_body_field(reason, false)?; } diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index 40cbf6794..ec99a3345 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -72,11 +72,5 @@ pub async fn feature_post( ) .await?; - build_post_response( - &context, - orig_post.community_id, - &local_user_view.person, - post_id, - ) - .await + build_post_response(&context, orig_post.community_id, local_user_view, post_id).await } diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index fccd9f8df..e6903fb3c 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -85,11 +85,5 @@ pub async fn like_post( ) .await?; - build_post_response( - context.deref(), - post.community_id, - &local_user_view.person, - post_id, - ) - .await + build_post_response(context.deref(), post.community_id, local_user_view, post_id).await } diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index 05db8ebbb..36f9c2a33 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -63,11 +63,5 @@ pub async fn lock_post( ) .await?; - build_post_response( - &context, - orig_post.community_id, - &local_user_view.person, - post_id, - ) - .await + build_post_response(&context, orig_post.community_id, local_user_view, post_id).await } diff --git a/crates/api/src/post/save.rs b/crates/api/src/post/save.rs index 96dd85579..85dfc11e3 100644 --- a/crates/api/src/post/save.rs +++ b/crates/api/src/post/save.rs @@ -34,9 +34,14 @@ pub async fn save_post( let post_id = data.post_id; let person_id = local_user_view.person.id; - let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let post_view = PostView::read( + &mut context.pool(), + post_id, + Some(&local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?; diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index 70d95e160..9f90aff99 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -10,6 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ comment::Comment, + local_user::LocalUser, moderator::{AdminPurgeComment, AdminPurgeCommentForm}, }, traits::Crud, @@ -29,9 +30,21 @@ pub async fn purge_comment( let comment_id = data.comment_id; // Read the comment to get the post_id and community - let comment_view = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; + + // Also check that you're a higher admin + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![comment_view.creator.id], + ) + .await?; let post_id = comment_view.comment.post_id; diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index 14b250681..59eded6ad 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -9,13 +9,16 @@ use lemmy_api_common::{ SuccessResponse, }; use lemmy_db_schema::{ + newtypes::PersonId, source::{ community::Community, + local_user::LocalUser, moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm}, }, traits::Crud, }; use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] @@ -32,6 +35,21 @@ pub async fn purge_community( .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?; + // Also check that you're a higher admin than all the mods + let community_mod_person_ids = + CommunityModeratorView::for_community(&mut context.pool(), community.id) + .await? + .iter() + .map(|cmv| cmv.moderator.id) + .collect::>(); + + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + community_mod_person_ids, + ) + .await?; + if let Some(banner) = &community.banner { purge_image_from_pictrs(banner, &context).await.ok(); } diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 1b38752c7..dc824b163 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -10,6 +10,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + local_user::LocalUser, moderator::{AdminPurgePerson, AdminPurgePersonForm}, person::{Person, PersonUpdateForm}, }, @@ -27,9 +28,18 @@ pub async fn purge_person( // Only let admin purge an item is_admin(&local_user_view)?; + // Also check that you're a higher admin + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![data.person_id], + ) + .await?; + let person = Person::read(&mut context.pool(), data.person_id) .await? .ok_or(LemmyErrorType::CouldntFindPerson)?; + ban_nonlocal_user_from_local_communities( &local_user_view, &person, diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index 75cd021d1..6e512312f 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -10,6 +10,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + local_user::LocalUser, moderator::{AdminPurgePost, AdminPurgePostForm}, post::Post, }, @@ -32,6 +33,14 @@ pub async fn purge_post( .await? .ok_or(LemmyErrorType::CouldntFindPost)?; + // Also check that you're a higher admin + LocalUser::is_higher_admin_check( + &mut context.pool(), + local_user_view.person.id, + vec![post.creator_id], + ) + .await?; + // Purge image if let Some(url) = &post.url { purge_image_from_pictrs(url, &context).await.ok(); diff --git a/crates/api/src/site/registration_applications/get.rs b/crates/api/src/site/registration_applications/get.rs new file mode 100644 index 000000000..2d5d6bf5b --- /dev/null +++ b/crates/api/src/site/registration_applications/get.rs @@ -0,0 +1,28 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + site::{GetRegistrationApplication, RegistrationApplicationResponse}, + utils::is_admin, +}; +use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + +/// Lists registration applications, filterable by undenied only. +pub async fn get_registration_application( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + // Read the view + let registration_application = + RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id) + .await? + .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; + + Ok(Json(RegistrationApplicationResponse { + registration_application, + })) +} diff --git a/crates/api/src/site/registration_applications/mod.rs b/crates/api/src/site/registration_applications/mod.rs index e5082615a..e904f597d 100644 --- a/crates/api/src/site/registration_applications/mod.rs +++ b/crates/api/src/site/registration_applications/mod.rs @@ -1,3 +1,4 @@ pub mod approve; +pub mod get; pub mod list; pub mod unread_count; diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs index 85cf065eb..200284b00 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -36,8 +36,8 @@ pub async fn build_comment_response( local_user_view: Option, recipient_ids: Vec, ) -> LemmyResult { - let person_id = local_user_view.map(|l| l.person.id); - let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id) + let local_user = local_user_view.map(|l| l.local_user); + let comment_view = CommentView::read(&mut context.pool(), comment_id, local_user.as_ref()) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; Ok(CommentResponse { @@ -54,11 +54,11 @@ pub async fn build_community_response( let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id) .await .is_ok(); - let person_id = local_user_view.person.id; + let local_user = local_user_view.local_user; let community_view = CommunityView::read( &mut context.pool(), community_id, - Some(person_id), + Some(&local_user), is_mod_or_admin, ) .await? @@ -74,16 +74,17 @@ pub async fn build_community_response( pub async fn build_post_response( context: &LemmyContext, community_id: CommunityId, - person: &Person, + local_user_view: LocalUserView, post_id: PostId, ) -> LemmyResult> { - let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), person, community_id) + let local_user = local_user_view.local_user; + let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id) .await .is_ok(); let post_view = PostView::read( &mut context.pool(), post_id, - Some(person.id), + Some(&local_user), is_mod_or_admin, ) .await? @@ -103,6 +104,7 @@ pub async fn send_local_notifs( let mut recipient_ids = Vec::new(); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); + // let person = my_local_user.person; // Read the comment view to get extra info let comment_view = CommentView::read(&mut context.pool(), comment_id, None) .await? diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 6c17d4e6a..905394785 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -116,10 +116,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("123456".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) .await diff --git a/crates/api_common/src/context.rs b/crates/api_common/src/context.rs index f4ac41db1..334983b20 100644 --- a/crates/api_common/src/context.rs +++ b/crates/api_common/src/context.rs @@ -55,7 +55,7 @@ impl LemmyContext { /// Initialize a context for use in tests which blocks federation network calls. /// /// Do not use this in production code. - pub async fn init_test_context() -> Data { + pub async fn init_test_federation_config() -> FederationConfig { // call this to run migrations let pool = build_db_pool_for_tests().await; @@ -70,14 +70,19 @@ impl LemmyContext { let rate_limit_cell = RateLimitCell::with_test_config(); let context = LemmyContext::create(pool, client, secret, rate_limit_cell.clone()); - let config = FederationConfig::builder() + + FederationConfig::builder() .domain(context.settings().hostname.clone()) .app_data(context) + .debug(true) // Dont allow any network fetches .http_fetch_limit(0) .build() .await - .expect("build federation config"); + .expect("build federation config") + } + pub async fn init_test_context() -> Data { + let config = Self::init_test_federation_config().await; config.to_request_data() } } diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index f42a468b5..74369173b 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -81,6 +81,8 @@ pub struct GetPosts { pub show_hidden: Option, /// If true, then show the read posts (even if your user setting is to hide them) pub show_read: Option, + /// If true, then show the nsfw posts (even if your user setting is to hide them) + pub show_nsfw: Option, pub page_cursor: Option, } diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 77bfcd8ee..3850de1c6 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -1,7 +1,15 @@ use crate::federate_retry_sleep_duration; use chrono::{DateTime, Utc}; use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, InstanceId, LanguageId, PersonId, PostId}, + newtypes::{ + CommentId, + CommunityId, + InstanceId, + LanguageId, + PersonId, + PostId, + RegistrationApplicationId, + }, source::{ federation_queue_state::FederationQueueState, instance::Instance, @@ -440,13 +448,22 @@ pub struct ListRegistrationApplicationsResponse { pub registration_applications: Vec, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Gets a registration application for a person +pub struct GetRegistrationApplication { + 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))] /// Approves a registration application. pub struct ApproveRegistrationApplication { - pub id: i32, + pub id: RegistrationApplicationId, pub approve: bool, pub deny_reason: Option, } diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index ff3efd946..b7c65740c 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -54,7 +54,7 @@ pub async fn create_comment( let post_view = PostView::read( &mut context.pool(), post_id, - Some(local_user_view.person.id), + Some(&local_user_view.local_user), true, ) .await? diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 2b1a20f89..29706d365 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -21,9 +21,13 @@ pub async fn delete_comment( local_user_view: LocalUserView, ) -> LemmyResult> { let comment_id = data.comment_id; - let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; // Dont delete it if its already been deleted. if orig_comment.comment.deleted == data.deleted { diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 926472b94..f810c8275 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -11,6 +11,7 @@ use lemmy_db_schema::{ source::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, + local_user::LocalUser, moderator::{ModRemoveComment, ModRemoveCommentForm}, }, traits::{Crud, Reportable}, @@ -25,9 +26,13 @@ pub async fn remove_comment( local_user_view: LocalUserView, ) -> LemmyResult> { let comment_id = data.comment_id; - let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_mod_action( &local_user_view.person, @@ -37,6 +42,14 @@ pub async fn remove_comment( ) .await?; + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + orig_comment.community.id, + local_user_view.person.id, + vec![orig_comment.creator.id], + ) + .await?; + // Don't allow removing or restoring comment which was deleted by user, as it would reveal // the comment text in mod log. if orig_comment.comment.deleted { @@ -68,14 +81,8 @@ pub async fn remove_comment( }; ModRemoveComment::create(&mut context.pool(), &form).await?; - let recipient_ids = send_local_notifs( - vec![], - comment_id, - &local_user_view.person.clone(), - false, - &context, - ) - .await?; + let recipient_ids = + send_local_notifs(vec![], comment_id, &local_user_view.person, false, &context).await?; let updated_comment_id = updated_comment.id; ActivityChannel::submit_activity( diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 4c8cf9436..32bd08240 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -36,9 +36,13 @@ pub async fn update_comment( let local_site = LocalSite::read(&mut context.pool()).await?; let comment_id = data.comment_id; - let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) - .await? - .ok_or(LemmyErrorType::CouldntFindComment)?; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await? + .ok_or(LemmyErrorType::CouldntFindComment)?; check_community_user_action( &local_user_view.person, diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 39ef8ce09..0f6abc2aa 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -188,5 +188,5 @@ pub async fn create_post( } }; - build_post_response(&context, community_id, &local_user_view.person, post_id).await + build_post_response(&context, community_id, local_user_view, post_id).await } diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs index 696566c8e..6834030ac 100644 --- a/crates/api_crud/src/post/delete.rs +++ b/crates/api_crud/src/post/delete.rs @@ -62,7 +62,7 @@ pub async fn delete_post( build_post_response( &context, orig_post.community_id, - &local_user_view.person, + local_user_view, data.post_id, ) .await diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 60b5609a9..ebf8940a2 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -55,9 +55,15 @@ pub async fn get_post( .await .is_ok(); - let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + let local_user = local_user_view.map(|l| l.local_user); + let post_view = PostView::read( + &mut context.pool(), + post_id, + local_user.as_ref(), + is_mod_or_admin, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; let post_id = post_view.post.id; if let Some(person_id) = person_id { @@ -76,20 +82,19 @@ pub async fn get_post( let community_view = CommunityView::read( &mut context.pool(), community_id, - person_id, + local_user.as_ref(), is_mod_or_admin, ) .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; - let local_user = local_user_view.as_ref().map(|u| &u.local_user); // Fetch the cross_posts let cross_posts = if let Some(url) = &post_view.post.url { let mut x_posts = PostQuery { url_search: Some(url.inner().as_str().into()), - local_user, + local_user: local_user.as_ref(), ..Default::default() } .list(&local_site.site, &mut context.pool()) diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index 682ed75d3..b4fdba6fb 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + local_user::LocalUser, moderator::{ModRemovePost, ModRemovePostForm}, post::{Post, PostUpdateForm}, post_report::PostReport, @@ -37,6 +38,14 @@ pub async fn remove_post( ) .await?; + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + orig_post.community_id, + local_user_view.person.id, + vec![orig_post.creator_id], + ) + .await?; + // Update the post let post_id = data.post_id; let removed = data.removed; @@ -73,11 +82,5 @@ pub async fn remove_post( ) .await?; - build_post_response( - &context, - orig_post.community_id, - &local_user_view.person, - post_id, - ) - .await + build_post_response(&context, orig_post.community_id, local_user_view, post_id).await } diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index 9e665aed6..cd4b2d41b 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -137,7 +137,7 @@ pub async fn update_post( build_post_response( context.deref(), orig_post.community_id, - &local_user_view.person, + local_user_view, post_id, ) .await diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 69e82007c..e8065c08b 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -84,7 +84,7 @@ pub async fn get_site( |pool| CommunityBlockView::for_person(pool, person_id), |pool| InstanceBlockView::for_person(pool, person_id), |pool| PersonBlockView::for_person(pool, person_id), - |pool| CommunityModeratorView::for_person(pool, person_id, true), + |pool| CommunityModeratorView::for_person(pool, person_id, Some(&local_user_view.local_user)), |pool| LocalUserLanguage::read(pool, local_user_id) )) .with_lemmy_type(LemmyErrorType::SystemErrLogin)?; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index c84bd0a50..64bef8760 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -150,18 +150,18 @@ pub async fn register( .unwrap_or(site_view.site.content_warning.is_some()); // Create the local user - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .email(data.email.as_deref().map(str::to_lowercase)) - .password_encrypted(data.password.to_string()) - .show_nsfw(Some(show_nsfw)) - .accepted_application(accepted_application) - .default_listing_type(Some(local_site.default_post_listing_type)) - .post_listing_mode(Some(local_site.default_post_listing_mode)) - .interface_language(language_tags.first().cloned()) + let local_user_form = LocalUserInsertForm { + email: data.email.as_deref().map(str::to_lowercase), + password_encrypted: data.password.to_string(), + show_nsfw: Some(show_nsfw), + accepted_application, + default_listing_type: Some(local_site.default_post_listing_type), + post_listing_mode: Some(local_site.default_post_listing_mode), + interface_language: language_tags.first().cloned(), // If its the initial site setup, they are an admin - .admin(Some(!local_site.site_setup)) - .build(); + admin: Some(!local_site.site_setup), + ..LocalUserInsertForm::new(inserted_person.id, data.password.to_string()) + }; let all_languages = Language::read_all(&mut context.pool()).await?; // use hashset to avoid duplicates diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index a9c6f657a..8f245d076 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -31,7 +31,7 @@ serde = { workspace = true } actix-web = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -strum_macros = { workspace = true } +strum = { workspace = true } url = { workspace = true } http = { workspace = true } futures = { workspace = true } diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index c00c87f4c..7ceafed8d 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -43,6 +43,7 @@ pub async fn list_posts( let saved_only = data.saved_only; let show_hidden = data.show_hidden; let show_read = data.show_read; + let show_nsfw = data.show_nsfw; let liked_only = data.liked_only; let disliked_only = data.disliked_only; @@ -84,6 +85,7 @@ pub async fn list_posts( limit, show_hidden, show_read, + show_nsfw, ..Default::default() } .list(&local_site.site, &mut context.pool()) diff --git a/crates/apub/src/api/read_community.rs b/crates/apub/src/api/read_community.rs index dae7719ae..62fd6ec0b 100644 --- a/crates/apub/src/api/read_community.rs +++ b/crates/apub/src/api/read_community.rs @@ -29,7 +29,7 @@ pub async fn get_community( check_private_instance(&local_user_view, &local_site)?; - let person_id = local_user_view.as_ref().map(|u| u.person.id); + let local_user = local_user_view.as_ref().map(|u| &u.local_user); let community_id = match data.id { Some(id) => id, @@ -53,7 +53,7 @@ pub async fn get_community( let community_view = CommunityView::read( &mut context.pool(), community_id, - person_id, + local_user, is_mod_or_admin, ) .await? diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index 149e06a17..d61f6d9c5 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -96,7 +96,7 @@ pub async fn read_person( let moderates = CommunityModeratorView::for_person( &mut context.pool(), person_details_id, - local_user_view.is_some(), + local_user_view.map(|l| l.local_user).as_ref(), ) .await?; diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index 47f6c5d06..3f2591241 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ site::{ResolveObject, ResolveObjectResponse}, utils::check_private_instance, }; -use lemmy_db_schema::{newtypes::PersonId, source::local_site::LocalSite, utils::DbPool}; +use lemmy_db_schema::{source::local_site::LocalSite, utils::DbPool}; use lemmy_db_views::structs::{CommentView, LocalUserView, PostView}; use lemmy_db_views_actor::structs::{CommunityView, PersonView}; use lemmy_utils::error::{LemmyErrorExt2, LemmyErrorType, LemmyResult}; @@ -23,10 +23,9 @@ pub async fn resolve_object( ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site)?; - let person_id = local_user_view.map(|v| v.person.id); // If we get a valid personId back we can safely assume that the user is authenticated, // if there's no personId then the JWT was missing or invalid. - let is_authenticated = person_id.is_some(); + let is_authenticated = local_user_view.is_some(); let res = if is_authenticated { // user is fully authenticated; allow remote lookups as well. @@ -37,24 +36,26 @@ pub async fn resolve_object( } .with_lemmy_type(LemmyErrorType::CouldntFindObject)?; - convert_response(res, person_id, &mut context.pool()) + convert_response(res, local_user_view, &mut context.pool()) .await .with_lemmy_type(LemmyErrorType::CouldntFindObject) } async fn convert_response( object: SearchableObjects, - user_id: Option, + local_user_view: Option, pool: &mut DbPool<'_>, ) -> LemmyResult> { use SearchableObjects::*; let removed_or_deleted; let mut res = ResolveObjectResponse::default(); + let local_user = local_user_view.map(|l| l.local_user); + match object { Post(p) => { removed_or_deleted = p.deleted || p.removed; res.post = Some( - PostView::read(pool, p.id, user_id, false) + PostView::read(pool, p.id, local_user.as_ref(), false) .await? .ok_or(LemmyErrorType::CouldntFindPost)?, ) @@ -62,7 +63,7 @@ async fn convert_response( Comment(c) => { removed_or_deleted = c.deleted || c.removed; res.comment = Some( - CommentView::read(pool, c.id, user_id) + CommentView::read(pool, c.id, local_user.as_ref()) .await? .ok_or(LemmyErrorType::CouldntFindComment)?, ) @@ -79,7 +80,7 @@ async fn convert_response( UserOrCommunity::Community(c) => { removed_or_deleted = c.deleted || c.removed; res.community = Some( - CommunityView::read(pool, c.id, user_id, false) + CommunityView::read(pool, c.id, local_user.as_ref(), false) .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?, ) diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 9f2cb58c5..a0879b3c9 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -345,10 +345,7 @@ mod tests { }; let person = Person::create(&mut context.pool(), &person_form).await?; - let user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("pass".to_string()) - .build(); + let user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?; Ok( diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index 24bb63b4c..6f0a7328d 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -109,7 +109,7 @@ impl Object for ApubSite { icon: self.icon.clone().map(ImageObject::new), image: self.banner.clone().map(ImageObject::new), inbox: self.inbox_url.clone().into(), - outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?, + outbox: Url::parse(&format!("{}site_outbox", self.actor_id))?, public_key: self.public_key(), language, content_warning: self.content_warning.clone(), diff --git a/crates/apub/src/protocol/activities/community/lock_page.rs b/crates/apub/src/protocol/activities/community/lock_page.rs index c60b86cf8..a08b3c5a2 100644 --- a/crates/apub/src/protocol/activities/community/lock_page.rs +++ b/crates/apub/src/protocol/activities/community/lock_page.rs @@ -13,10 +13,10 @@ use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::Crud}; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use serde::{Deserialize, Serialize}; -use strum_macros::Display; +use strum::Display; use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, Display)] +#[derive(Clone, Debug, Display, Deserialize, Serialize)] pub enum LockType { Lock, } diff --git a/crates/apub/src/protocol/activities/mod.rs b/crates/apub/src/protocol/activities/mod.rs index ce0808ee9..d1da73a16 100644 --- a/crates/apub/src/protocol/activities/mod.rs +++ b/crates/apub/src/protocol/activities/mod.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use strum_macros::Display; +use strum::Display; pub mod block; pub mod community; diff --git a/crates/apub/src/protocol/activities/voting/vote.rs b/crates/apub/src/protocol/activities/voting/vote.rs index b632333c7..9fae264a5 100644 --- a/crates/apub/src/protocol/activities/voting/vote.rs +++ b/crates/apub/src/protocol/activities/voting/vote.rs @@ -8,7 +8,7 @@ use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; use lemmy_api_common::context::LemmyContext; use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; -use strum_macros::Display; +use strum::Display; use url::Url; #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index c153e51c3..dbcd0482c 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -46,7 +46,6 @@ serde = { workspace = true } serde_with = { workspace = true } url = { workspace = true } strum = { workspace = true } -strum_macros = { workspace = true } serde_json = { workspace = true, optional = true } activitypub_federation = { workspace = true, optional = true } lemmy_utils = { workspace = true, optional = true } diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index 8483d6c20..5a8658baf 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -533,10 +533,7 @@ mod tests { let person_form = PersonInsertForm::test_form(instance.id, "my test person"); let person = Person::create(pool, &person_form).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("my_pw".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await @@ -645,10 +642,7 @@ mod tests { let person_form = PersonInsertForm::test_form(instance.id, "my test person"); let person = Person::create(pool, &person_form).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("my_pw".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await .unwrap(); diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 6cd90cc66..eaf35a90d 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -1,7 +1,14 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId}, - schema::{community, community_follower, instance}, + schema::{ + community, + community_follower, + community_moderator, + community_person_ban, + instance, + post, + }, source::{ actor_language::CommunityLanguage, community::{ @@ -42,6 +49,7 @@ use diesel::{ Queryable, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[async_trait] impl Crud for Community { @@ -83,9 +91,8 @@ impl Joinable for CommunityModerator { pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, ) -> Result { - use crate::schema::community_moderator::dsl::community_moderator; let conn = &mut get_conn(pool).await?; - insert_into(community_moderator) + insert_into(community_moderator::table) .values(community_moderator_form) .get_result::(conn) .await @@ -95,9 +102,8 @@ impl Joinable for CommunityModerator { pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, ) -> Result { - use crate::schema::community_moderator::dsl::community_moderator; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.find(( + diesel::delete(community_moderator::table.find(( community_moderator_form.person_id, community_moderator_form.community_id, ))) @@ -147,25 +153,23 @@ impl Community { pool: &mut DbPool<'_>, url: &DbUrl, ) -> Result, Error> { - use crate::schema::community::dsl::{featured_url, moderators_url}; - use CollectionType::*; let conn = &mut get_conn(pool).await?; let res = community::table - .filter(moderators_url.eq(url)) + .filter(community::moderators_url.eq(url)) .first(conn) .await .optional()?; if let Some(c) = res { - Ok(Some((c, Moderators))) + Ok(Some((c, CollectionType::Moderators))) } else { let res = community::table - .filter(featured_url.eq(url)) + .filter(community::featured_url.eq(url)) .first(conn) .await .optional()?; if let Some(c) = res { - Ok(Some((c, Featured))) + Ok(Some((c, CollectionType::Featured))) } else { Ok(None) } @@ -177,7 +181,6 @@ impl Community { posts: Vec, pool: &mut DbPool<'_>, ) -> Result<(), Error> { - use crate::schema::post; let conn = &mut get_conn(pool).await?; for p in &posts { debug_assert!(p.community_id == community_id); @@ -185,10 +188,10 @@ impl Community { // Mark the given posts as featured and all other posts as not featured. let post_ids = posts.iter().map(|p| p.id); update(post::table) - .filter(post::dsl::community_id.eq(community_id)) + .filter(post::community_id.eq(community_id)) // This filter is just for performance - .filter(post::dsl::featured_community.or(post::dsl::id.eq_any(post_ids.clone()))) - .set(post::dsl::featured_community.eq(post::dsl::id.eq_any(post_ids))) + .filter(post::featured_community.or(post::id.eq_any(post_ids.clone()))) + .set(post::featured_community.eq(post::id.eq_any(post_ids))) .execute(conn) .await?; Ok(()) @@ -200,37 +203,68 @@ impl CommunityModerator { pool: &mut DbPool<'_>, for_community_id: CommunityId, ) -> Result { - use crate::schema::community_moderator::dsl::{community_id, community_moderator}; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.filter(community_id.eq(for_community_id))) - .execute(conn) - .await + diesel::delete( + community_moderator::table.filter(community_moderator::community_id.eq(for_community_id)), + ) + .execute(conn) + .await } pub async fn leave_all_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, ) -> Result { - use crate::schema::community_moderator::dsl::{community_moderator, person_id}; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.filter(person_id.eq(for_person_id))) - .execute(conn) - .await + diesel::delete( + community_moderator::table.filter(community_moderator::person_id.eq(for_person_id)), + ) + .execute(conn) + .await } pub async fn get_person_moderated_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, ) -> Result, Error> { - use crate::schema::community_moderator::dsl::{community_id, community_moderator, person_id}; let conn = &mut get_conn(pool).await?; - community_moderator - .filter(person_id.eq(for_person_id)) - .select(community_id) + community_moderator::table + .filter(community_moderator::person_id.eq(for_person_id)) + .select(community_moderator::community_id) .load::(conn) .await } + + /// Checks to make sure the acting moderator was added earlier than the target moderator + pub async fn is_higher_mod_check( + pool: &mut DbPool<'_>, + for_community_id: CommunityId, + mod_person_id: PersonId, + target_person_ids: Vec, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + + // Build the list of persons + let mut persons = target_person_ids; + persons.push(mod_person_id); + persons.dedup(); + + let res = community_moderator::table + .filter(community_moderator::community_id.eq(for_community_id)) + .filter(community_moderator::person_id.eq_any(persons)) + .order_by(community_moderator::published) + // This does a limit 1 select first + .first::(conn) + .await?; + + // If the first result sorted by published is the acting mod + if res.person_id == mod_person_id { + Ok(()) + } else { + Err(LemmyErrorType::NotHigherMod)? + } + } } #[async_trait] @@ -240,11 +274,13 @@ impl Bannable for CommunityPersonBan { pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, ) -> Result { - use crate::schema::community_person_ban::dsl::{community_id, community_person_ban, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(community_person_ban) + insert_into(community_person_ban::table) .values(community_person_ban_form) - .on_conflict((community_id, person_id)) + .on_conflict(( + community_person_ban::community_id, + community_person_ban::person_id, + )) .do_update() .set(community_person_ban_form) .get_result::(conn) @@ -255,9 +291,8 @@ impl Bannable for CommunityPersonBan { pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, ) -> Result { - use crate::schema::community_person_ban::dsl::community_person_ban; let conn = &mut get_conn(pool).await?; - diesel::delete(community_person_ban.find(( + diesel::delete(community_person_ban::table.find(( community_person_ban_form.person_id, community_person_ban_form.community_id, ))) @@ -291,11 +326,10 @@ impl CommunityFollower { pool: &mut DbPool<'_>, remote_community_id: CommunityId, ) -> Result { - use crate::schema::community_follower::dsl::{community_follower, community_id}; let conn = &mut get_conn(pool).await?; - select(exists( - community_follower.filter(community_id.eq(remote_community_id)), - )) + select(exists(community_follower::table.filter( + community_follower::community_id.eq(remote_community_id), + ))) .get_result(conn) .await } @@ -316,11 +350,13 @@ impl Queryable, Pg> for SubscribedType { impl Followable for CommunityFollower { type Form = CommunityFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { - use crate::schema::community_follower::dsl::{community_follower, community_id, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(community_follower) + insert_into(community_follower::table) .values(form) - .on_conflict((community_id, person_id)) + .on_conflict(( + community_follower::community_id, + community_follower::person_id, + )) .do_update() .set(form) .get_result::(conn) @@ -331,17 +367,16 @@ impl Followable for CommunityFollower { community_id: CommunityId, person_id: PersonId, ) -> Result { - use crate::schema::community_follower::dsl::{community_follower, pending}; let conn = &mut get_conn(pool).await?; - diesel::update(community_follower.find((person_id, community_id))) - .set(pending.eq(false)) + diesel::update(community_follower::table.find((person_id, community_id))) + .set(community_follower::pending.eq(false)) .get_result::(conn) .await } + async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { - use crate::schema::community_follower::dsl::community_follower; let conn = &mut get_conn(pool).await?; - diesel::delete(community_follower.find((form.person_id, form.community_id))) + diesel::delete(community_follower::table.find((form.person_id, form.community_id))) .execute(conn) .await } @@ -397,10 +432,8 @@ impl ApubActor for Community { } #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { - use crate::{ source::{ community::{ @@ -415,28 +448,30 @@ mod tests { CommunityUpdateForm, }, instance::Instance, + local_user::LocalUser, person::{Person, PersonInsertForm}, }, traits::{Bannable, Crud, Followable, Joinable}, utils::build_db_pool_for_tests, CommunityVisibility, }; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; 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, "bobbee"); + let bobby_person = PersonInsertForm::test_form(inserted_instance.id, "bobby"); + let inserted_bobby = Person::create(pool, &bobby_person).await?; - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let artemis_person = PersonInsertForm::test_form(inserted_instance.id, "artemis"); + let inserted_artemis = Person::create(pool, &artemis_person).await?; let new_community = CommunityInsertForm::builder() .name("TIL".into()) @@ -445,7 +480,7 @@ mod tests { .instance_id(inserted_instance.id) .build(); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let expected_community = Community { id: inserted_community.id, @@ -477,91 +512,120 @@ mod tests { let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, pending: false, }; - let inserted_community_follower = CommunityFollower::follow(pool, &community_follower_form) - .await - .unwrap(); + let inserted_community_follower = + CommunityFollower::follow(pool, &community_follower_form).await?; let expected_community_follower = CommunityFollower { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, pending: false, published: inserted_community_follower.published, }; - let community_moderator_form = CommunityModeratorForm { + let bobby_moderator_form = CommunityModeratorForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, }; - let inserted_community_moderator = CommunityModerator::join(pool, &community_moderator_form) - .await - .unwrap(); + let inserted_bobby_moderator = CommunityModerator::join(pool, &bobby_moderator_form).await?; + + let artemis_moderator_form = CommunityModeratorForm { + community_id: inserted_community.id, + person_id: inserted_artemis.id, + }; + + let _inserted_artemis_moderator = + CommunityModerator::join(pool, &artemis_moderator_form).await?; let expected_community_moderator = CommunityModerator { community_id: inserted_community.id, - person_id: inserted_person.id, - published: inserted_community_moderator.published, + person_id: inserted_bobby.id, + published: inserted_bobby_moderator.published, }; + let moderator_person_ids = vec![inserted_bobby.id, inserted_artemis.id]; + + // Make sure bobby is marked as a higher mod than artemis, and vice versa + let bobby_higher_check = CommunityModerator::is_higher_mod_check( + pool, + inserted_community.id, + inserted_bobby.id, + moderator_person_ids.clone(), + ) + .await; + assert!(bobby_higher_check.is_ok()); + + // Also check the other is_higher_mod_or_admin function just in case + let bobby_higher_check_2 = LocalUser::is_higher_mod_or_admin_check( + pool, + inserted_community.id, + inserted_bobby.id, + moderator_person_ids.clone(), + ) + .await; + assert!(bobby_higher_check_2.is_ok()); + + // This should throw an error, since artemis was added later + let artemis_higher_check = CommunityModerator::is_higher_mod_check( + pool, + inserted_community.id, + inserted_artemis.id, + moderator_person_ids, + ) + .await; + assert!(artemis_higher_check.is_err()); + let community_person_ban_form = CommunityPersonBanForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, expires: None, }; - let inserted_community_person_ban = CommunityPersonBan::ban(pool, &community_person_ban_form) - .await - .unwrap(); + let inserted_community_person_ban = + CommunityPersonBan::ban(pool, &community_person_ban_form).await?; let expected_community_person_ban = CommunityPersonBan { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, published: inserted_community_person_ban.published, expires: None, }; let read_community = Community::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let update_community_form = CommunityUpdateForm { title: Some("nada".to_owned()), ..Default::default() }; - let updated_community = Community::update(pool, inserted_community.id, &update_community_form) - .await - .unwrap(); + let updated_community = + Community::update(pool, inserted_community.id, &update_community_form).await?; - let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form) - .await - .unwrap(); - let left_community = CommunityModerator::leave(pool, &community_moderator_form) - .await - .unwrap(); - let unban = CommunityPersonBan::unban(pool, &community_person_ban_form) - .await - .unwrap(); - let num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form).await?; + let left_community = CommunityModerator::leave(pool, &bobby_moderator_form).await?; + let unban = CommunityPersonBan::unban(pool, &community_person_ban_form).await?; + let num_deleted = Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_bobby.id).await?; + Person::delete(pool, inserted_artemis.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_community, read_community); assert_eq!(expected_community, inserted_community); assert_eq!(expected_community, updated_community); assert_eq!(expected_community_follower, inserted_community_follower); - assert_eq!(expected_community_moderator, inserted_community_moderator); + assert_eq!(expected_community_moderator, inserted_bobby_moderator); assert_eq!(expected_community_person_ban, inserted_community_person_ban); assert_eq!(1, ignored_community); assert_eq!(1, left_community); assert_eq!(1, unban); // assert_eq!(2, loaded_count); assert_eq!(1, num_deleted); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 9b59e07ba..32e8d50b7 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -1,6 +1,6 @@ use crate::{ - newtypes::{DbUrl, LanguageId, LocalUserId, PersonId}, - schema::{local_user, person, registration_application}, + newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId}, + schema::{community, community_moderator, local_user, person, registration_application}, source::{ actor_language::LocalUserLanguage, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, @@ -13,16 +13,19 @@ use crate::{ now, DbPool, }, + CommunityVisibility, }; use bcrypt::{hash, DEFAULT_COST}; use diesel::{ dsl::{insert_into, not, IntervalDsl}, result::Error, + CombineDsl, ExpressionMethods, JoinOnDsl, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl LocalUser { pub async fn create( @@ -215,6 +218,72 @@ impl LocalUser { blocked_instances, }) } + + /// Checks to make sure the acting admin is higher than the target admin + pub async fn is_higher_admin_check( + pool: &mut DbPool<'_>, + admin_person_id: PersonId, + target_person_ids: Vec, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + + // Build the list of persons + let mut persons = target_person_ids; + persons.push(admin_person_id); + persons.dedup(); + + let res = local_user::table + .filter(local_user::admin.eq(true)) + .filter(local_user::person_id.eq_any(persons)) + .order_by(local_user::id) + // This does a limit 1 select first + .first::(conn) + .await?; + + // If the first result sorted by published is the acting admin + if res.person_id == admin_person_id { + Ok(()) + } else { + Err(LemmyErrorType::NotHigherAdmin)? + } + } + + /// Checks to make sure the acting moderator is higher than the target moderator + pub async fn is_higher_mod_or_admin_check( + pool: &mut DbPool<'_>, + for_community_id: CommunityId, + admin_person_id: PersonId, + target_person_ids: Vec, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + + // Build the list of persons + let mut persons = target_person_ids; + persons.push(admin_person_id); + persons.dedup(); + + let admins = local_user::table + .filter(local_user::admin.eq(true)) + .filter(local_user::person_id.eq_any(&persons)) + .order_by(local_user::id) + .select(local_user::person_id); + + let mods = community_moderator::table + .filter(community_moderator::community_id.eq(for_community_id)) + .filter(community_moderator::person_id.eq_any(&persons)) + .order_by(community_moderator::published) + .select(community_moderator::person_id); + + let res = admins.union_all(mods).get_results::(conn).await?; + let first_person = res.as_slice().first().ok_or(LemmyErrorType::NotHigherMod)?; + + // If the first result sorted by published is the acting mod + if *first_person == admin_person_id { + Ok(()) + } else { + Err(LemmyErrorType::NotHigherMod)? + } + } } /// Adds some helper functions for an optional LocalUser @@ -225,6 +294,12 @@ pub trait LocalUserOptionHelper { fn show_read_posts(&self) -> bool; fn is_admin(&self) -> bool; fn show_nsfw(&self, site: &Site) -> bool; + fn visible_communities_only(&self, query: Q) -> Q + where + Q: diesel::query_dsl::methods::FilterDsl< + diesel::dsl::Eq, + Output = Q, + >; } impl LocalUserOptionHelper for Option<&LocalUser> { @@ -253,14 +328,32 @@ impl LocalUserOptionHelper for Option<&LocalUser> { .map(|l| l.show_nsfw) .unwrap_or(site.content_warning.is_some()) } + + fn visible_communities_only(&self, query: Q) -> Q + where + Q: diesel::query_dsl::methods::FilterDsl< + diesel::dsl::Eq, + Output = Q, + >, + { + if self.is_none() { + query.filter(community::visibility.eq(CommunityVisibility::Public)) + } else { + query + } + } } impl LocalUserInsertForm { pub fn test_form(person_id: PersonId) -> Self { - Self::builder() - .person_id(person_id) - .password_encrypted(String::new()) - .build() + Self::new(person_id, String::new()) + } + + pub fn test_form_admin(person_id: PersonId) -> Self { + LocalUserInsertForm { + admin: Some(true), + ..Self::test_form(person_id) + } } } @@ -272,3 +365,58 @@ pub struct UserBackupLists { pub blocked_users: Vec, pub blocked_instances: Vec, } + +#[cfg(test)] +#[allow(clippy::indexing_slicing)] +mod tests { + use crate::{ + source::{ + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + }, + traits::Crud, + utils::build_db_pool_for_tests, + }; + use lemmy_utils::error::LemmyResult; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn test_admin_higher_check() -> 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?; + + let fiona_person = PersonInsertForm::test_form(inserted_instance.id, "fiona"); + let inserted_fiona_person = Person::create(pool, &fiona_person).await?; + + let fiona_local_user_form = LocalUserInsertForm::test_form_admin(inserted_fiona_person.id); + let _inserted_fiona_local_user = + LocalUser::create(pool, &fiona_local_user_form, vec![]).await?; + + let delores_person = PersonInsertForm::test_form(inserted_instance.id, "delores"); + let inserted_delores_person = Person::create(pool, &delores_person).await?; + let delores_local_user_form = LocalUserInsertForm::test_form_admin(inserted_delores_person.id); + let _inserted_delores_local_user = + LocalUser::create(pool, &delores_local_user_form, vec![]).await?; + + let admin_person_ids = vec![inserted_fiona_person.id, inserted_delores_person.id]; + + // Make sure fiona is marked as a higher admin than delores, and vice versa + let fiona_higher_check = + LocalUser::is_higher_admin_check(pool, inserted_fiona_person.id, admin_person_ids.clone()) + .await; + assert!(fiona_higher_check.is_ok()); + + // This should throw an error, since delores was added later + let delores_higher_check = + LocalUser::is_higher_admin_check(pool, inserted_delores_person.id, admin_person_ids).await; + assert!(delores_higher_check.is_err()); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } +} diff --git a/crates/db_schema/src/impls/local_user_vote_display_mode.rs b/crates/db_schema/src/impls/local_user_vote_display_mode.rs index d77502335..2d169f81b 100644 --- a/crates/db_schema/src/impls/local_user_vote_display_mode.rs +++ b/crates/db_schema/src/impls/local_user_vote_display_mode.rs @@ -31,6 +31,7 @@ impl LocalUserVoteDisplayMode { .get_result::(conn) .await } + pub async fn update( pool: &mut DbPool<'_>, local_user_id: LocalUserId, diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index 0b1351af1..be05ed8ac 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -72,10 +72,7 @@ mod tests { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy prw"); let inserted_person = Person::create(pool, &new_person).await?; - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("pass".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; // Create password reset token diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index f318a503a..89c108f8c 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -182,11 +182,10 @@ impl ApubActor for Person { impl Followable for PersonFollower { type Form = PersonFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { - use crate::schema::person_follower::dsl::{follower_id, person_follower, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(person_follower) + insert_into(person_follower::table) .values(form) - .on_conflict((follower_id, person_id)) + .on_conflict((person_follower::follower_id, person_follower::person_id)) .do_update() .set(form) .get_result::(conn) @@ -196,9 +195,8 @@ impl Followable for PersonFollower { unimplemented!() } async fn unfollow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { - use crate::schema::person_follower::dsl::person_follower; let conn = &mut get_conn(pool).await?; - diesel::delete(person_follower.find((form.follower_id, form.person_id))) + diesel::delete(person_follower::table.find((form.follower_id, form.person_id))) .execute(conn) .await } @@ -220,7 +218,6 @@ impl PersonFollower { } #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { @@ -232,22 +229,21 @@ mod tests { traits::{Crud, Followable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; 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, "holly"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let expected_person = Person { id: inserted_person.id, @@ -274,57 +270,54 @@ mod tests { }; let read_person = Person::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let update_person_form = PersonUpdateForm { actor_id: Some(inserted_person.actor_id.clone()), ..Default::default() }; - let updated_person = Person::update(pool, inserted_person.id, &update_person_form) - .await - .unwrap(); + let updated_person = Person::update(pool, inserted_person.id, &update_person_form).await?; - let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let num_deleted = Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_person, read_person); assert_eq!(expected_person, inserted_person); assert_eq!(expected_person, updated_person); assert_eq!(1, num_deleted); + + Ok(()) } #[tokio::test] #[serial] - async fn follow() { + async fn follow() -> 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 person_form_1 = PersonInsertForm::test_form(inserted_instance.id, "erich"); - let person_1 = Person::create(pool, &person_form_1).await.unwrap(); + let person_1 = Person::create(pool, &person_form_1).await?; let person_form_2 = PersonInsertForm::test_form(inserted_instance.id, "michele"); - let person_2 = Person::create(pool, &person_form_2).await.unwrap(); + let person_2 = Person::create(pool, &person_form_2).await?; let follow_form = PersonFollowerForm { person_id: person_1.id, follower_id: person_2.id, pending: false, }; - let person_follower = PersonFollower::follow(pool, &follow_form).await.unwrap(); + let person_follower = PersonFollower::follow(pool, &follow_form).await?; assert_eq!(person_1.id, person_follower.person_id); assert_eq!(person_2.id, person_follower.follower_id); assert!(!person_follower.pending); - let followers = PersonFollower::list_followers(pool, person_1.id) - .await - .unwrap(); + let followers = PersonFollower::list_followers(pool, person_1.id).await?; assert_eq!(vec![person_2], followers); - let unfollow = PersonFollower::unfollow(pool, &follow_form).await.unwrap(); + let unfollow = PersonFollower::unfollow(pool, &follow_form).await?; assert_eq!(1, unfollow); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/registration_application.rs b/crates/db_schema/src/impls/registration_application.rs index 46b7d4bee..055ffb51f 100644 --- a/crates/db_schema/src/impls/registration_application.rs +++ b/crates/db_schema/src/impls/registration_application.rs @@ -1,6 +1,6 @@ use crate::{ diesel::OptionalExtension, - newtypes::LocalUserId, + newtypes::{LocalUserId, RegistrationApplicationId}, schema::registration_application::dsl::{local_user_id, registration_application}, source::registration_application::{ RegistrationApplication, @@ -17,7 +17,7 @@ use diesel_async::RunQueryDsl; impl Crud for RegistrationApplication { type InsertForm = RegistrationApplicationInsertForm; type UpdateForm = RegistrationApplicationUpdateForm; - type IdType = i32; + type IdType = RegistrationApplicationId; async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 30a1ea644..9d46d5d96 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -43,7 +43,7 @@ pub mod utils; pub mod schema_setup; use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumString}; +use strum::{Display, EnumString}; #[cfg(feature = "full")] use ts_rs::TS; diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c5c9e8e84..c715305bb 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -107,7 +107,7 @@ pub struct PrivateMessageReportId(i32); #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The site id. -pub struct SiteId(i32); +pub struct SiteId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] @@ -148,6 +148,12 @@ pub struct LocalSiteId(i32); /// The custom emoji id. pub struct CustomEmojiId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The registration application id. +pub struct RegistrationApplicationId(i32); + #[cfg(feature = "full")] #[derive(Serialize, Deserialize)] #[serde(remote = "Ltree")] diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 5d592ddf1..c7a5b5224 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -69,38 +68,59 @@ pub struct LocalUser { pub collapse_bot_comments: bool, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable))] #[cfg_attr(feature = "full", diesel(table_name = local_user))] pub struct LocalUserInsertForm { - #[builder(!default)] pub person_id: PersonId, - #[builder(!default)] pub password_encrypted: String, + #[new(default)] pub email: Option, + #[new(default)] pub show_nsfw: Option, + #[new(default)] pub theme: Option, + #[new(default)] pub default_sort_type: Option, + #[new(default)] pub default_listing_type: Option, + #[new(default)] pub interface_language: Option, + #[new(default)] pub show_avatars: Option, + #[new(default)] pub send_notifications_to_email: Option, + #[new(default)] pub show_bot_accounts: Option, + #[new(default)] pub show_scores: Option, + #[new(default)] pub show_read_posts: Option, + #[new(default)] pub email_verified: Option, + #[new(default)] pub accepted_application: Option, + #[new(default)] pub totp_2fa_secret: Option>, + #[new(default)] pub open_links_in_new_tab: Option, + #[new(default)] pub blur_nsfw: Option, + #[new(default)] pub auto_expand: Option, + #[new(default)] pub infinite_scroll_enabled: Option, + #[new(default)] pub admin: Option, + #[new(default)] pub post_listing_mode: Option, + #[new(default)] pub totp_2fa_enabled: Option, + #[new(default)] pub enable_keyboard_navigation: Option, + #[new(default)] pub enable_animated_images: Option, + #[new(default)] pub collapse_bot_comments: Option, } diff --git a/crates/db_schema/src/source/registration_application.rs b/crates/db_schema/src/source/registration_application.rs index 2824951d7..2ac973f34 100644 --- a/crates/db_schema/src/source/registration_application.rs +++ b/crates/db_schema/src/source/registration_application.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{LocalUserId, PersonId}; +use crate::newtypes::{LocalUserId, PersonId, RegistrationApplicationId}; #[cfg(feature = "full")] use crate::schema::registration_application; use chrono::{DateTime, Utc}; @@ -15,7 +15,7 @@ use ts_rs::TS; #[cfg_attr(feature = "full", ts(export))] /// A registration application. pub struct RegistrationApplication { - pub id: i32, + pub id: RegistrationApplicationId, pub local_user_id: LocalUserId, pub answer: String, pub admin_id: Option, diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 41a61380e..72c6873cd 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,6 +1,6 @@ use crate::{ diesel::ExpressionMethods, - newtypes::{DbUrl, PersonId}, + newtypes::{DbUrl, CommentSortType, PersonId}, schema::community, schema_setup, CommentSortType, @@ -10,7 +10,6 @@ use crate::{ use chrono::{DateTime, TimeDelta, Utc}; use deadpool::Runtime; use diesel::{ - dsl, helper_types::AsExprOf, pg::Pg, query_builder::{Query, QueryFragment}, @@ -336,10 +335,6 @@ fn establish_connection(config: &str) -> BoxFuture Queries { } } -pub fn visible_communities_only(my_person_id: Option, query: Q) -> Q -where - Q: diesel::query_dsl::methods::FilterDsl< - dsl::Eq, - Output = Q, - >, -{ - if my_person_id.is_none() { - query.filter(community::visibility.eq(CommunityVisibility::Public)) - } else { - query - } -} - #[cfg(test)] #[allow(clippy::indexing_slicing)] mod tests { diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 950d061ba..d7b26a1ed 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -301,10 +301,7 @@ mod tests { let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_timmy.id) - .password_encrypted("123".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) .await .unwrap(); diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index cd7560a00..e2752a0c7 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -36,22 +36,13 @@ use lemmy_db_schema::{ post, }, source::local_user::LocalUser, - utils::{ - fuzzy_search, - limit_and_offset, - visible_communities_only, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, ListingType, }; fn queries<'a>() -> Queries< - impl ReadFn<'a, CommentView, (CommentId, Option)>, + impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>, impl ListFn<'a, CommentView, CommentQuery<'a>>, > { let is_creator_banned_from_community = exists( @@ -182,9 +173,12 @@ fn queries<'a>() -> Queries< }; let read = move |mut conn: DbConn<'a>, - (comment_id, my_person_id): (CommentId, Option)| async move { - let mut query = all_joins(comment::table.find(comment_id).into_boxed(), my_person_id); - query = visible_communities_only(my_person_id, query); + (comment_id, my_local_user): (CommentId, Option<&'a LocalUser>)| async move { + let mut query = all_joins( + comment::table.find(comment_id).into_boxed(), + my_local_user.person_id(), + ); + query = my_local_user.visible_communities_only(query); query.first(&mut conn).await }; @@ -301,7 +295,7 @@ fn queries<'a>() -> Queries< query = query.filter(not(is_creator_blocked(person_id_join))); }; - query = visible_communities_only(options.local_user.person_id(), query); + query = options.local_user.visible_communities_only(query); // A Max depth given means its a tree fetch let (limit, offset) = if let Some(max_depth) = options.max_depth { @@ -366,16 +360,16 @@ fn queries<'a>() -> Queries< } impl CommentView { - pub async fn read( + pub async fn read<'a>( pool: &mut DbPool<'_>, comment_id: CommentId, - my_person_id: Option, + my_local_user: Option<&'a LocalUser>, ) -> Result, Error> { // If a person is given, then my_vote (res.9), if None, should be 0, not null // Necessary to differentiate between other person's votes - if let Ok(Some(res)) = queries().read(pool, (comment_id, my_person_id)).await { + if let Ok(Some(res)) = queries().read(pool, (comment_id, my_local_user)).await { let mut new_view = res.clone(); - if my_person_id.is_some() && res.my_vote.is_none() { + if my_local_user.is_some() && res.my_vote.is_none() { new_view.my_vote = Some(0); } if res.comment.deleted || res.comment.removed { @@ -490,11 +484,8 @@ mod tests { let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy"); let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?; - let timmy_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_timmy_person.id) - .admin(Some(true)) - .password_encrypted(String::new()) - .build(); + 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?; let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara"); @@ -676,7 +667,7 @@ mod tests { let read_comment_from_blocked_person = CommentView::read( pool, data.inserted_comment_1.id, - Some(data.timmy_local_user_view.person.id), + Some(&data.timmy_local_user_view.local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; @@ -1171,7 +1162,7 @@ mod tests { let authenticated_comment = CommentView::read( pool, data.inserted_comment_0.id, - Some(data.timmy_local_user_view.person.id), + Some(&data.timmy_local_user_view.local_user), ) .await; assert!(authenticated_comment.is_ok()); @@ -1211,7 +1202,7 @@ mod tests { let comment_view = CommentView::read( pool, data.inserted_comment_0.id, - Some(inserted_banned_from_comm_local_user.person_id), + Some(&inserted_banned_from_comm_local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; @@ -1232,7 +1223,7 @@ mod tests { let comment_view = CommentView::read( pool, data.inserted_comment_0.id, - Some(data.timmy_local_user_view.person.id), + Some(&data.timmy_local_user_view.local_user), ) .await? .ok_or(LemmyErrorType::CouldntFindComment)?; diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index e89b7d545..0cd06dd4e 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -323,10 +323,7 @@ mod tests { let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_timmy.id) - .password_encrypted("123".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) .await .unwrap(); diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 0e22f689b..0ec7e0a5d 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -49,7 +49,6 @@ use lemmy_db_schema::{ get_conn, limit_and_offset, now, - visible_communities_only, Commented, DbConn, DbPool, @@ -64,7 +63,7 @@ use lemmy_db_schema::{ use tracing::debug; fn queries<'a>() -> Queries< - impl ReadFn<'a, PostView, (PostId, Option, bool)>, + impl ReadFn<'a, PostView, (PostId, Option<&'a LocalUser>, bool)>, impl ListFn<'a, PostView, (PostQuery<'a>, &'a Site)>, > { let is_creator_banned_from_community = exists( @@ -142,6 +141,7 @@ fn queries<'a>() -> Queries< .single_value() }; + // TODO maybe this should go to localuser also let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option| { let is_local_user_banned_from_community_selection: Box< @@ -250,52 +250,56 @@ fn queries<'a>() -> Queries< )) }; - let read = - move |mut conn: DbConn<'a>, - (post_id, my_person_id, is_mod_or_admin): (PostId, Option, bool)| async move { - // The left join below will return None in this case - let person_id_join = my_person_id.unwrap_or(PersonId(-1)); + let read = move |mut conn: DbConn<'a>, + (post_id, my_local_user, is_mod_or_admin): ( + PostId, + Option<&'a LocalUser>, + bool, + )| async move { + // The left join below will return None in this case + let my_person_id = my_local_user.person_id(); + let person_id_join = my_person_id.unwrap_or(PersonId(-1)); - let mut query = all_joins( - post_aggregates::table - .filter(post_aggregates::post_id.eq(post_id)) - .into_boxed(), - my_person_id, - ); + let mut query = all_joins( + post_aggregates::table + .filter(post_aggregates::post_id.eq(post_id)) + .into_boxed(), + my_person_id, + ); - // Hide deleted and removed for non-admins or mods - if !is_mod_or_admin { - query = query - .filter( - community::removed - .eq(false) - .or(post::creator_id.eq(person_id_join)), - ) - .filter( - post::removed - .eq(false) - .or(post::creator_id.eq(person_id_join)), - ) - // users can see their own deleted posts - .filter( - community::deleted - .eq(false) - .or(post::creator_id.eq(person_id_join)), - ) - .filter( - post::deleted - .eq(false) - .or(post::creator_id.eq(person_id_join)), - ); - } + // Hide deleted and removed for non-admins or mods + if !is_mod_or_admin { + query = query + .filter( + community::removed + .eq(false) + .or(post::creator_id.eq(person_id_join)), + ) + .filter( + post::removed + .eq(false) + .or(post::creator_id.eq(person_id_join)), + ) + // users can see their own deleted posts + .filter( + community::deleted + .eq(false) + .or(post::creator_id.eq(person_id_join)), + ) + .filter( + post::deleted + .eq(false) + .or(post::creator_id.eq(person_id_join)), + ); + } - query = visible_communities_only(my_person_id, query); + query = my_local_user.visible_communities_only(query); - Commented::new(query) - .text("PostView::read") - .first(&mut conn) - .await - }; + Commented::new(query) + .text("PostView::read") + .first(&mut conn) + .await + }; let list = move |mut conn: DbConn<'a>, (options, site): (PostQuery<'a>, &'a Site)| async move { // The left join below will return None in this case @@ -392,7 +396,10 @@ fn queries<'a>() -> Queries< .filter(not(post::removed.or(post::deleted))); } - if !options.local_user.show_nsfw(site) { + if !options + .show_nsfw + .unwrap_or(options.local_user.show_nsfw(site)) + { query = query .filter(post::nsfw.eq(false)) .filter(community::nsfw.eq(false)); @@ -437,7 +444,7 @@ fn queries<'a>() -> Queries< } }; - query = visible_communities_only(options.local_user.person_id(), query); + query = options.local_user.visible_communities_only(query); // Dont filter blocks or missing languages for moderator view type if let (Some(person_id), false) = ( @@ -552,14 +559,14 @@ fn queries<'a>() -> Queries< } impl PostView { - pub async fn read( + pub async fn read<'a>( pool: &mut DbPool<'_>, post_id: PostId, - my_person_id: Option, + my_local_user: Option<&'a LocalUser>, is_mod_or_admin: bool, ) -> Result, Error> { queries() - .read(pool, (post_id, my_person_id, is_mod_or_admin)) + .read(pool, (post_id, my_local_user, is_mod_or_admin)) .await } } @@ -617,6 +624,7 @@ pub struct PostQuery<'a> { pub page_back: Option, pub show_hidden: Option, pub show_read: Option, + pub show_nsfw: Option, } impl<'a> PostQuery<'a> { @@ -938,7 +946,7 @@ mod tests { let post_listing_single_with_person = PostView::read( pool, data.inserted_post.id, - Some(data.local_user_view.person.id), + Some(&data.local_user_view.local_user), false, ) .await? @@ -1067,7 +1075,7 @@ mod tests { let post_listing_single_with_person = PostView::read( pool, data.inserted_post.id, - Some(data.local_user_view.person.id), + Some(&data.local_user_view.local_user), false, ) .await? @@ -1585,6 +1593,48 @@ mod tests { cleanup(data, pool).await } + #[tokio::test] + #[serial] + async fn post_listings_hide_nsfw() -> LemmyResult<()> { + let pool = &build_db_pool().await?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Mark a post as nsfw + let update_form = PostUpdateForm { + nsfw: Some(true), + ..Default::default() + }; + + Post::update(pool, data.inserted_bot_post.id, &update_form).await?; + + // Make sure you don't see the nsfw post in the regular results + let post_listings_hide_nsfw = data.default_post_query().list(&data.site, pool).await?; + assert_eq!(vec![POST], names(&post_listings_hide_nsfw)); + + // Make sure it does come back with the show_nsfw option + let post_listings_show_nsfw = PostQuery { + sort: Some(SortType::New), + show_nsfw: Some(true), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + 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() + .ok_or(LemmyErrorType::CouldntFindPost)? + .post + .nsfw + ); + + cleanup(data, pool).await + } + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { let num_deleted = Post::delete(pool, data.inserted_post.id).await?; Community::delete(pool, data.inserted_community.id).await?; @@ -1755,7 +1805,7 @@ mod tests { let authenticated_post = PostView::read( pool, data.inserted_post.id, - Some(data.local_user_view.person.id), + Some(&data.local_user_view.local_user), false, ) .await; @@ -1797,7 +1847,7 @@ mod tests { let post_view = PostView::read( pool, data.inserted_post.id, - Some(inserted_banned_from_comm_local_user.person_id), + Some(&inserted_banned_from_comm_local_user), false, ) .await? @@ -1819,7 +1869,7 @@ mod tests { let post_view = PostView::read( pool, data.inserted_post.id, - Some(data.local_user_view.person.id), + Some(&data.local_user_view.local_user), false, ) .await? diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index cd63859af..54c7f7598 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -11,12 +11,18 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, + newtypes::{PersonId, RegistrationApplicationId}, schema::{local_user, person, registration_application}, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, }; +enum ReadBy { + Id(RegistrationApplicationId), + Person(PersonId), +} + fn queries<'a>() -> Queries< - impl ReadFn<'a, RegistrationApplicationView, i32>, + impl ReadFn<'a, RegistrationApplicationView, ReadBy>, impl ListFn<'a, RegistrationApplicationView, RegistrationApplicationQuery>, > { let all_joins = |query: registration_application::BoxedQuery<'a, Pg>| { @@ -36,14 +42,15 @@ fn queries<'a>() -> Queries< )) }; - let read = move |mut conn: DbConn<'a>, registration_application_id: i32| async move { - all_joins( - registration_application::table - .find(registration_application_id) - .into_boxed(), - ) - .first(&mut conn) - .await + let read = move |mut conn: DbConn<'a>, search: ReadBy| async move { + let mut query = all_joins(registration_application::table.into_boxed()); + + query = match search { + ReadBy::Id(id) => query.filter(registration_application::id.eq(id)), + ReadBy::Person(person_id) => query.filter(person::id.eq(person_id)), + }; + + query.first(&mut conn).await }; let list = move |mut conn: DbConn<'a>, options: RegistrationApplicationQuery| async move { @@ -76,11 +83,17 @@ fn queries<'a>() -> Queries< impl RegistrationApplicationView { pub async fn read( pool: &mut DbPool<'_>, - registration_application_id: i32, + id: RegistrationApplicationId, ) -> Result, Error> { - queries().read(pool, registration_application_id).await + queries().read(pool, ReadBy::Id(id)).await } + pub async fn read_by_person( + pool: &mut DbPool<'_>, + person_id: PersonId, + ) -> Result, Error> { + queries().read(pool, ReadBy::Person(person_id)).await + } /// Returns the current unread registration_application count pub async fn get_unread_count( pool: &mut DbPool<'_>, @@ -167,11 +180,7 @@ mod tests { let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap(); - let timmy_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_timmy_person.id) - .password_encrypted("nada".to_string()) - .admin(Some(true)) - .build(); + 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 @@ -181,10 +190,7 @@ mod tests { let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap(); - let sara_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_sara_person.id) - .password_encrypted("nada".to_string()) - .build(); + 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 @@ -209,10 +215,7 @@ mod tests { let inserted_jess_person = Person::create(pool, &jess_person_form).await.unwrap(); - let jess_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_jess_person.id) - .password_encrypted("nada".to_string()) - .build(); + 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 diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index d9e6a3352..af139b8b2 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -33,7 +33,6 @@ serde_with = { workspace = true } ts-rs = { workspace = true, optional = true } chrono.workspace = true strum = { workspace = true } -strum_macros = { workspace = true } [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_views_actor/src/community_moderator_view.rs b/crates/db_views_actor/src/community_moderator_view.rs index f58e3fee0..f2a59fd9f 100644 --- a/crates/db_views_actor/src/community_moderator_view.rs +++ b/crates/db_views_actor/src/community_moderator_view.rs @@ -2,10 +2,11 @@ use crate::structs::CommunityModeratorView; use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ + impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, schema::{community, community_moderator, person}, + source::local_user::LocalUser, utils::{get_conn, DbPool}, - CommunityVisibility, }; impl CommunityModeratorView { @@ -60,20 +61,28 @@ impl CommunityModeratorView { pub async fn for_person( pool: &mut DbPool<'_>, person_id: PersonId, - is_authenticated: bool, + local_user: Option<&LocalUser>, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let mut query = community_moderator::table .inner_join(community::table) .inner_join(person::table) .filter(community_moderator::person_id.eq(person_id)) - .filter(community::deleted.eq(false)) - .filter(community::removed.eq(false)) .select((community::all_columns, person::all_columns)) .into_boxed(); - if !is_authenticated { - query = query.filter(community::visibility.eq(CommunityVisibility::Public)); + + query = local_user.visible_communities_only(query); + + // only show deleted communities to creator + if Some(person_id) != local_user.person_id() { + query = query.filter(community::deleted.eq(false)); } + + // Show removed communities to admins only + if !local_user.is_admin() { + query = query.filter(community::removed.eq(false)) + } + query.load::(conn).await } diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 25e76c7b3..0e731878a 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -22,27 +22,18 @@ use lemmy_db_schema::{ instance_block, }, source::{community::CommunityFollower, local_user::LocalUser, site::Site}, - utils::{ - fuzzy_search, - limit_and_offset, - visible_communities_only, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, ListingType, SortType, }; fn queries<'a>() -> Queries< - impl ReadFn<'a, CommunityView, (CommunityId, Option, bool)>, + impl ReadFn<'a, CommunityView, (CommunityId, Option<&'a LocalUser>, bool)>, impl ListFn<'a, CommunityView, (CommunityQuery<'a>, &'a Site)>, > { - let all_joins = |query: community::BoxedQuery<'a, Pg>, my_person_id: Option| { + let all_joins = |query: community::BoxedQuery<'a, Pg>, my_local_user: Option<&'a LocalUser>| { // The left join below will return None in this case - let person_id_join = my_person_id.unwrap_or(PersonId(-1)); + let person_id_join = my_local_user.person_id().unwrap_or(PersonId(-1)); query .inner_join(community_aggregates::table) @@ -89,14 +80,14 @@ fn queries<'a>() -> Queries< .and(community::deleted.eq(false)); let read = move |mut conn: DbConn<'a>, - (community_id, my_person_id, is_mod_or_admin): ( + (community_id, my_local_user, is_mod_or_admin): ( CommunityId, - Option, + Option<&'a LocalUser>, bool, )| async move { let mut query = all_joins( community::table.find(community_id).into_boxed(), - my_person_id, + my_local_user, ) .select(selection); @@ -105,7 +96,7 @@ fn queries<'a>() -> Queries< query = query.filter(not_removed_or_deleted); } - query = visible_communities_only(my_person_id, query); + query = my_local_user.visible_communities_only(query); query.first(&mut conn).await }; @@ -116,11 +107,7 @@ fn queries<'a>() -> Queries< // The left join below will return None in this case let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); - let mut query = all_joins( - community::table.into_boxed(), - options.local_user.person_id(), - ) - .select(selection); + let mut query = all_joins(community::table.into_boxed(), options.local_user).select(selection); if let Some(search_term) = options.search_term { let searcher = fuzzy_search(&search_term); @@ -173,7 +160,7 @@ fn queries<'a>() -> Queries< query = query.filter(community::nsfw.eq(false)); } - query = visible_communities_only(options.local_user.person_id(), query); + query = options.local_user.visible_communities_only(query); let (limit, offset) = limit_and_offset(options.page, options.limit)?; query @@ -187,14 +174,14 @@ fn queries<'a>() -> Queries< } impl CommunityView { - pub async fn read( + pub async fn read<'a>( pool: &mut DbPool<'_>, community_id: CommunityId, - my_person_id: Option, + my_local_user: Option<&'a LocalUser>, is_mod_or_admin: bool, ) -> Result, Error> { queries() - .read(pool, (community_id, my_person_id, is_mod_or_admin)) + .read(pool, (community_id, my_local_user, is_mod_or_admin)) .await } @@ -288,10 +275,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted(String::new()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await .unwrap(); @@ -388,7 +372,7 @@ mod tests { let authenticated_community = CommunityView::read( pool, data.inserted_community.id, - Some(data.local_user.person_id), + Some(&data.local_user), false, ) .await; diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 98a0ca38d..7a2edfb44 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ SortType, }; use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumString}; +use strum::{Display, EnumString}; enum ListMode { Admins, @@ -196,10 +196,7 @@ mod tests { ..PersonInsertForm::test_form(inserted_instance.id, "alice") }; let alice = Person::create(pool, &alice_form).await?; - let alice_local_user_form = LocalUserInsertForm::builder() - .person_id(alice.id) - .password_encrypted(String::new()) - .build(); + let alice_local_user_form = LocalUserInsertForm::test_form(alice.id); let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?; let bob_form = PersonInsertForm { @@ -208,10 +205,7 @@ mod tests { ..PersonInsertForm::test_form(inserted_instance.id, "bob") }; let bob = Person::create(pool, &bob_form).await?; - let bob_local_user_form = LocalUserInsertForm::builder() - .person_id(bob.id) - .password_encrypted(String::new()) - .build(); + let bob_local_user_form = LocalUserInsertForm::test_form(bob.id); let bob_local_user = LocalUser::create(pool, &bob_local_user_form, vec![]).await?; Ok(Data { diff --git a/crates/federate/Cargo.toml b/crates/federate/Cargo.toml index 2405d3af0..b8b438901 100644 --- a/crates/federate/Cargo.toml +++ b/crates/federate/Cargo.toml @@ -34,6 +34,13 @@ tokio = { workspace = true, features = ["full"] } tracing.workspace = true moka.workspace = true tokio-util = "0.7.11" +async-trait.workspace = true [dev-dependencies] serial_test = { workspace = true } +url.workspace = true +actix-web.workspace = true +tracing-test = "0.2.5" +uuid.workspace = true +test-context = "0.3.0" +mockall = "0.12.1" diff --git a/crates/federate/src/inboxes.rs b/crates/federate/src/inboxes.rs new file mode 100644 index 000000000..9869e5270 --- /dev/null +++ b/crates/federate/src/inboxes.rs @@ -0,0 +1,572 @@ +use crate::util::LEMMY_TEST_FAST_FEDERATION; +use anyhow::Result; +use async_trait::async_trait; +use chrono::{DateTime, TimeZone, Utc}; +use lemmy_db_schema::{ + newtypes::{CommunityId, DbUrl, InstanceId}, + source::{activity::SentActivity, site::Site}, + utils::{ActualDbPool, DbPool}, +}; +use lemmy_db_views_actor::structs::CommunityFollowerView; +use once_cell::sync::Lazy; +use reqwest::Url; +use std::collections::{HashMap, HashSet}; + +/// interval with which new additions to community_followers are queried. +/// +/// The first time some user on an instance follows a specific remote community (or, more precisely: +/// the first time a (followed_community_id, follower_inbox_url) tuple appears), this delay limits +/// the maximum time until the follow actually results in activities from that community id being +/// sent to that inbox url. This delay currently needs to not be too small because the DB load is +/// currently fairly high because of the current structure of storing inboxes for every person, not +/// having a separate list of shared_inboxes, and the architecture of having every instance queue be +/// fully separate. (see https://github.com/LemmyNet/lemmy/issues/3958) +static FOLLOW_ADDITIONS_RECHECK_DELAY: Lazy = Lazy::new(|| { + if *LEMMY_TEST_FAST_FEDERATION { + chrono::TimeDelta::try_seconds(1).expect("TimeDelta out of bounds") + } else { + chrono::TimeDelta::try_minutes(2).expect("TimeDelta out of bounds") + } +}); +/// The same as FOLLOW_ADDITIONS_RECHECK_DELAY, but triggering when the last person on an instance +/// unfollows a specific remote community. This is expected to happen pretty rarely and updating it +/// in a timely manner is not too important. +static FOLLOW_REMOVALS_RECHECK_DELAY: Lazy = + Lazy::new(|| chrono::TimeDelta::try_hours(1).expect("TimeDelta out of bounds")); + +#[async_trait] +pub trait DataSource: Send + Sync { + async fn read_site_from_instance_id( + &self, + instance_id: InstanceId, + ) -> Result, diesel::result::Error>; + async fn get_instance_followed_community_inboxes( + &self, + instance_id: InstanceId, + last_fetch: DateTime, + ) -> Result, diesel::result::Error>; +} +pub struct DbDataSource { + pool: ActualDbPool, +} + +impl DbDataSource { + pub fn new(pool: ActualDbPool) -> Self { + Self { pool } + } +} + +#[async_trait] +impl DataSource for DbDataSource { + async fn read_site_from_instance_id( + &self, + instance_id: InstanceId, + ) -> Result, diesel::result::Error> { + Site::read_from_instance_id(&mut DbPool::Pool(&self.pool), instance_id).await + } + + async fn get_instance_followed_community_inboxes( + &self, + instance_id: InstanceId, + last_fetch: DateTime, + ) -> Result, diesel::result::Error> { + CommunityFollowerView::get_instance_followed_community_inboxes( + &mut DbPool::Pool(&self.pool), + instance_id, + last_fetch, + ) + .await + } +} + +pub(crate) struct CommunityInboxCollector { + // load site lazily because if an instance is first seen due to being on allowlist, + // the corresponding row in `site` may not exist yet since that is only added once + // `fetch_instance_actor_for_object` is called. + // (this should be unlikely to be relevant outside of the federation tests) + site_loaded: bool, + site: Option, + followed_communities: HashMap>, + last_full_communities_fetch: DateTime, + last_incremental_communities_fetch: DateTime, + instance_id: InstanceId, + domain: String, + pub(crate) data_source: T, +} + +pub type RealCommunityInboxCollector = CommunityInboxCollector; + +impl CommunityInboxCollector { + pub fn new_real( + pool: ActualDbPool, + instance_id: InstanceId, + domain: String, + ) -> RealCommunityInboxCollector { + CommunityInboxCollector::new(DbDataSource::new(pool), instance_id, domain) + } + pub fn new( + data_source: T, + instance_id: InstanceId, + domain: String, + ) -> CommunityInboxCollector { + CommunityInboxCollector { + data_source, + site_loaded: false, + site: None, + followed_communities: HashMap::new(), + last_full_communities_fetch: Utc.timestamp_nanos(0), + last_incremental_communities_fetch: Utc.timestamp_nanos(0), + instance_id, + domain, + } + } + /// get inbox urls of sending the given activity to the given instance + /// most often this will return 0 values (if instance doesn't care about the activity) + /// or 1 value (the shared inbox) + /// > 1 values only happens for non-lemmy software + pub async fn get_inbox_urls(&mut self, activity: &SentActivity) -> Result> { + let mut inbox_urls: HashSet = HashSet::new(); + + if activity.send_all_instances { + if !self.site_loaded { + self.site = self + .data_source + .read_site_from_instance_id(self.instance_id) + .await?; + self.site_loaded = true; + } + if let Some(site) = &self.site { + // Nutomic: Most non-lemmy software wont have a site row. That means it cant handle these + // activities. So handling it like this is fine. + inbox_urls.insert(site.inbox_url.inner().clone()); + } + } + if let Some(t) = &activity.send_community_followers_of { + if let Some(urls) = self.followed_communities.get(t) { + inbox_urls.extend(urls.iter().cloned()); + } + } + inbox_urls.extend( + activity + .send_inboxes + .iter() + .filter_map(std::option::Option::as_ref) + // a similar filter also happens within the activitypub-federation crate. but that filter + // happens much later - by doing it here, we can ensure that in the happy case, this + // function returns 0 urls which means the system doesn't have to create a tokio + // task for sending at all (since that task has a fair amount of overhead) + .filter(|&u| (u.domain() == Some(&self.domain))) + .map(|u| u.inner().clone()), + ); + tracing::trace!( + "get_inbox_urls: {:?}, send_inboxes: {:?}", + inbox_urls, + activity.send_inboxes + ); + Ok(inbox_urls.into_iter().collect()) + } + + pub async fn update_communities(&mut self) -> Result<()> { + if (Utc::now() - self.last_full_communities_fetch) > *FOLLOW_REMOVALS_RECHECK_DELAY { + tracing::debug!("{}: fetching full list of communities", self.domain); + // process removals every hour + (self.followed_communities, self.last_full_communities_fetch) = self + .get_communities(self.instance_id, Utc.timestamp_nanos(0)) + .await?; + self.last_incremental_communities_fetch = self.last_full_communities_fetch; + } + if (Utc::now() - self.last_incremental_communities_fetch) > *FOLLOW_ADDITIONS_RECHECK_DELAY { + // process additions every minute + let (news, time) = self + .get_communities(self.instance_id, self.last_incremental_communities_fetch) + .await?; + if !news.is_empty() { + tracing::debug!( + "{}: fetched {} incremental new followed communities", + self.domain, + news.len() + ); + } + self.followed_communities.extend(news); + self.last_incremental_communities_fetch = time; + } + Ok(()) + } + + /// get a list of local communities with the remote inboxes on the given instance that cares about + /// them + async fn get_communities( + &mut self, + instance_id: InstanceId, + last_fetch: DateTime, + ) -> Result<(HashMap>, DateTime)> { + // update to time before fetch to ensure overlap. subtract some time to ensure overlap even if + // published date is not exact + let new_last_fetch = Utc::now() - *FOLLOW_ADDITIONS_RECHECK_DELAY / 2; + + let inboxes = self + .data_source + .get_instance_followed_community_inboxes(instance_id, last_fetch) + .await?; + + let map: HashMap> = + inboxes.into_iter().fold(HashMap::new(), |mut map, (c, u)| { + map.entry(c).or_default().insert(u.into()); + map + }); + + Ok((map, new_last_fetch)) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +#[allow(clippy::indexing_slicing)] +mod tests { + use super::*; + use lemmy_db_schema::{ + newtypes::{ActivityId, CommunityId, InstanceId, SiteId}, + source::activity::{ActorType, SentActivity}, + }; + use mockall::{mock, predicate::*}; + use serde_json::json; + mock! { + DataSource {} + #[async_trait] + impl DataSource for DataSource { + async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> Result, diesel::result::Error>; + async fn get_instance_followed_community_inboxes( + &self, + instance_id: InstanceId, + last_fetch: DateTime, + ) -> Result, diesel::result::Error>; + } + } + + fn setup_collector() -> CommunityInboxCollector { + let mock_data_source = MockDataSource::new(); + let instance_id = InstanceId(1); + let domain = "example.com".to_string(); + CommunityInboxCollector::new(mock_data_source, instance_id, domain) + } + + #[tokio::test] + async fn test_get_inbox_urls_empty() { + let mut collector = setup_collector(); + let activity = SentActivity { + id: ActivityId(1), + ap_id: Url::parse("https://example.com/activities/1") + .unwrap() + .into(), + data: json!({}), + sensitive: false, + published: Utc::now(), + send_inboxes: vec![], + send_community_followers_of: None, + send_all_instances: false, + actor_type: ActorType::Person, + actor_apub_id: None, + }; + + let result = collector.get_inbox_urls(&activity).await.unwrap(); + assert!(result.is_empty()); + } + + #[tokio::test] + async fn test_get_inbox_urls_send_all_instances() { + let mut collector = setup_collector(); + let site_inbox = Url::parse("https://example.com/inbox").unwrap(); + let site = Site { + id: SiteId(1), + name: "Test Site".to_string(), + sidebar: None, + published: Utc::now(), + updated: None, + icon: None, + banner: None, + description: None, + actor_id: Url::parse("https://example.com/site").unwrap().into(), + last_refreshed_at: Utc::now(), + inbox_url: site_inbox.clone().into(), + private_key: None, + public_key: "test_key".to_string(), + instance_id: InstanceId(1), + content_warning: None, + }; + + collector + .data_source + .expect_read_site_from_instance_id() + .return_once(move |_| Ok(Some(site))); + + let activity = SentActivity { + id: ActivityId(1), + ap_id: Url::parse("https://example.com/activities/1") + .unwrap() + .into(), + data: json!({}), + sensitive: false, + published: Utc::now(), + send_inboxes: vec![], + send_community_followers_of: None, + send_all_instances: true, + actor_type: ActorType::Person, + actor_apub_id: None, + }; + + let result = collector.get_inbox_urls(&activity).await.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0], site_inbox); + } + + #[tokio::test] + async fn test_get_inbox_urls_community_followers() { + let mut collector = setup_collector(); + let community_id = CommunityId(1); + let url1 = "https://follower1.example.com/inbox"; + let url2 = "https://follower2.example.com/inbox"; + + collector + .data_source + .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()), + ]) + }); + + collector.update_communities().await.unwrap(); + + let activity = SentActivity { + id: ActivityId(1), + ap_id: Url::parse("https://example.com/activities/1") + .unwrap() + .into(), + data: json!({}), + sensitive: false, + published: Utc::now(), + send_inboxes: vec![], + send_community_followers_of: Some(community_id), + send_all_instances: false, + actor_type: ActorType::Person, + actor_apub_id: None, + }; + + let result = collector.get_inbox_urls(&activity).await.unwrap(); + assert_eq!(result.len(), 2); + assert!(result.contains(&Url::parse(url1).unwrap())); + assert!(result.contains(&Url::parse(url2).unwrap())); + } + + #[tokio::test] + async fn test_get_inbox_urls_send_inboxes() { + 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 activity = SentActivity { + id: ActivityId(1), + ap_id: Url::parse("https://example.com/activities/1") + .unwrap() + .into(), + data: json!({}), + sensitive: false, + published: Utc::now(), + send_inboxes: vec![ + Some(inbox_user_1.clone().into()), + Some(inbox_user_2.clone().into()), + Some(other_domain_inbox.clone().into()), + ], + send_community_followers_of: None, + send_all_instances: false, + actor_type: ActorType::Person, + actor_apub_id: None, + }; + + let result = collector.get_inbox_urls(&activity).await.unwrap(); + assert_eq!(result.len(), 2); + assert!(result.contains(&inbox_user_1)); + assert!(result.contains(&inbox_user_2)); + assert!(!result.contains(&other_domain_inbox)); + } + + #[tokio::test] + async fn test_get_inbox_urls_combined() { + 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 = Site { + id: SiteId(1), + name: "Test Site".to_string(), + sidebar: None, + published: Utc::now(), + updated: None, + icon: None, + banner: None, + description: None, + actor_id: Url::parse("https://example.com/site").unwrap().into(), + last_refreshed_at: Utc::now(), + inbox_url: site_inbox.clone().into(), + private_key: None, + public_key: "test_key".to_string(), + instance_id: InstanceId(1), + content_warning: None, + }; + + collector + .data_source + .expect_read_site_from_instance_id() + .return_once(move |_| Ok(Some(site))); + + let subdomain_inbox = "https://follower.example.com/inbox"; + collector + .data_source + .expect_get_instance_followed_community_inboxes() + .return_once(move |_, _| { + Ok(vec![( + community_id, + Url::parse(subdomain_inbox).unwrap().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(); + let activity = SentActivity { + id: ActivityId(1), + ap_id: Url::parse("https://example.com/activities/1") + .unwrap() + .into(), + data: json!({}), + sensitive: false, + published: Utc::now(), + send_inboxes: vec![ + Some(user1_inbox.clone().into()), + Some(user2_inbox.clone().into()), + ], + send_community_followers_of: Some(community_id), + send_all_instances: true, + actor_type: ActorType::Person, + actor_apub_id: None, + }; + + let result = collector.get_inbox_urls(&activity).await.unwrap(); + assert_eq!(result.len(), 3); + assert!(result.contains(&site_inbox)); + assert!(result.contains(&Url::parse(subdomain_inbox).unwrap())); + assert!(result.contains(&user1_inbox)); + assert!(!result.contains(&user2_inbox)); + } + + #[tokio::test] + async fn test_update_communities() { + 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 user2_inbox_str = "https://follower2.example.com/inbox"; + let user2_inbox = Url::parse(user2_inbox_str).unwrap(); + let user3_inbox_str = "https://follower3.example.com/inbox"; + let user3_inbox = Url::parse(user3_inbox_str).unwrap(); + + collector + .data_source + .expect_get_instance_followed_community_inboxes() + .times(2) + .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()), + ]) + } else { + Ok(vec![( + community_id3, + Url::parse(user3_inbox_str).unwrap().into(), + )]) + } + }); + + // First update + collector.update_communities().await.unwrap(); + 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_incremental_communities_fetch = + Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap(); + + // Second update (incremental) + collector.update_communities().await.unwrap(); + 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)); + } + + #[tokio::test] + async fn test_get_inbox_urls_no_duplicates() { + 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_clone = site_inbox.clone(); + let site = Site { + id: SiteId(1), + name: "Test Site".to_string(), + sidebar: None, + published: Utc::now(), + updated: None, + icon: None, + banner: None, + description: None, + actor_id: Url::parse("https://example.com/site").unwrap().into(), + last_refreshed_at: Utc::now(), + inbox_url: site_inbox.clone().into(), + private_key: None, + public_key: "test_key".to_string(), + instance_id: InstanceId(1), + content_warning: None, + }; + + collector + .data_source + .expect_read_site_from_instance_id() + .return_once(move |_| Ok(Some(site))); + + collector + .data_source + .expect_get_instance_followed_community_inboxes() + .return_once(move |_, _| Ok(vec![(community_id, site_inbox_clone.into())])); + + collector.update_communities().await.unwrap(); + + let activity = SentActivity { + id: ActivityId(1), + ap_id: Url::parse("https://example.com/activities/1") + .unwrap() + .into(), + data: json!({}), + sensitive: false, + published: Utc::now(), + send_inboxes: vec![Some(site_inbox.into())], + send_community_followers_of: Some(community_id), + send_all_instances: true, + actor_type: ActorType::Person, + actor_apub_id: None, + }; + + let result = collector.get_inbox_urls(&activity).await.unwrap(); + assert_eq!(result.len(), 1); + assert!(result.contains(&Url::parse("https://example.com/site_inbox").unwrap())); + } +} diff --git a/crates/federate/src/lib.rs b/crates/federate/src/lib.rs index 21b9229b5..66c0a2872 100644 --- a/crates/federate/src/lib.rs +++ b/crates/federate/src/lib.rs @@ -1,6 +1,9 @@ use crate::{util::CancellableTask, worker::InstanceWorker}; use activitypub_federation::config::FederationConfig; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{ + context::LemmyContext, + lemmy_utils::settings::structs::FederationWorkerConfig, +}; use lemmy_db_schema::{newtypes::InstanceId, source::instance::Instance}; use lemmy_utils::error::LemmyResult; use stats::receive_print_stats; @@ -14,6 +17,8 @@ use tokio_util::sync::CancellationToken; use tracing::info; use util::FederationQueueStateWithDomain; +mod inboxes; +mod send; mod stats; mod util; mod worker; @@ -38,10 +43,15 @@ pub struct SendManager { context: FederationConfig, stats_sender: UnboundedSender, exit_print: JoinHandle<()>, + federation_worker_config: FederationWorkerConfig, } impl SendManager { - fn new(opts: Opts, context: FederationConfig) -> Self { + fn new( + opts: Opts, + context: FederationConfig, + federation_worker_config: FederationWorkerConfig, + ) -> Self { assert!(opts.process_count > 0); assert!(opts.process_index > 0); assert!(opts.process_index <= opts.process_count); @@ -56,14 +66,20 @@ impl SendManager { stats_receiver, )), context, + federation_worker_config, } } - pub fn run(opts: Opts, context: FederationConfig) -> CancellableTask { + pub fn run( + opts: Opts, + context: FederationConfig, + config: FederationWorkerConfig, + ) -> CancellableTask { CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |cancel| { let opts = opts.clone(); + let config = config.clone(); let context = context.clone(); - let mut manager = Self::new(opts, context); + let mut manager = Self::new(opts, context, config); async move { let result = manager.do_loop(cancel).await; // the loop function will only return if there is (a) an internal error (e.g. db connection @@ -120,22 +136,21 @@ impl SendManager { // create new worker let context = self.context.clone(); let stats_sender = self.stats_sender.clone(); + let federation_worker_config = self.federation_worker_config.clone(); + self.workers.insert( instance.id, CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| { - // if the instance worker ends unexpectedly due to internal/db errors, this lambda is rerun by cancellabletask. + // if the instance worker ends unexpectedly due to internal/db errors, this lambda is + // rerun by cancellabletask. let instance = instance.clone(); - let req_data = context.to_request_data(); - let stats_sender = stats_sender.clone(); - async move { - InstanceWorker::init_and_loop( - instance, - req_data, - stop, - stats_sender, - ) - .await - } + InstanceWorker::init_and_loop( + instance, + context.clone(), + federation_worker_config.clone(), + stop, + stats_sender.clone(), + ) }), ); } else if !should_federate { @@ -214,7 +229,14 @@ mod test { .app_data(context.clone()) .build() .await?; + let concurrent_sends_per_instance = std::env::var("LEMMY_TEST_FEDERATION_CONCURRENT_SENDS") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(1); + let federation_worker_config = FederationWorkerConfig { + concurrent_sends_per_instance, + }; let pool = &mut context.pool(); let instances = vec![ Instance::read_or_create(pool, "alpha.com".to_string()).await?, @@ -222,7 +244,7 @@ mod test { Instance::read_or_create(pool, "gamma.com".to_string()).await?, ]; - let send_manager = SendManager::new(opts, federation_config); + let send_manager = SendManager::new(opts, federation_config, federation_worker_config); Ok(Self { send_manager, context, diff --git a/crates/federate/src/send.rs b/crates/federate/src/send.rs new file mode 100644 index 000000000..01d620eb0 --- /dev/null +++ b/crates/federate/src/send.rs @@ -0,0 +1,148 @@ +use crate::util::get_actor_cached; +use activitypub_federation::{ + activity_sending::SendActivityTask, + config::Data, + protocol::context::WithContext, +}; +use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; +use lemmy_api_common::{context::LemmyContext, federate_retry_sleep_duration}; +use lemmy_apub::{activity_lists::SharedInboxActivities, FEDERATION_CONTEXT}; +use lemmy_db_schema::{newtypes::ActivityId, source::activity::SentActivity}; +use reqwest::Url; +use std::ops::Deref; +use tokio::{sync::mpsc::UnboundedSender, time::sleep}; +use tokio_util::sync::CancellationToken; + +#[derive(Debug, Eq)] +pub(crate) struct SendSuccessInfo { + pub activity_id: ActivityId, + pub published: Option>, + // true if the activity was skipped because the target instance is not interested in this + // activity + pub was_skipped: bool, +} +impl PartialEq for SendSuccessInfo { + fn eq(&self, other: &Self) -> bool { + self.activity_id == other.activity_id + } +} +/// order backwards because the binary heap is a max heap, and we need the smallest element to be on +/// top +impl PartialOrd for SendSuccessInfo { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for SendSuccessInfo { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + other.activity_id.cmp(&self.activity_id) + } +} + +/// Represents the result of sending an activity. +/// +/// This enum is used to communicate the outcome of a send operation from a send task +/// to the main instance worker. It's designed to maintain a clean separation between +/// the send task and the main thread, allowing the send.rs file to be self-contained +/// and easier to understand. +/// +/// The use of a channel for communication (rather than shared atomic variables) was chosen +/// because: +/// 1. It keeps the send task cleanly separated with no direct interaction with the main thread. +/// 2. The failure event needs to be transferred to the main task for database updates anyway. +/// 3. The main fail_count should only be updated under certain conditions, which are best handled +/// in the main task. +/// 4. It maintains consistency in how data is communicated (all via channels rather than a mix of +/// channels and atomics). +/// 5. It simplifies concurrency management and makes the flow of data more predictable. +pub(crate) enum SendActivityResult { + Success(SendSuccessInfo), + Failure { fail_count: i32 }, +} +/// Represents a task for retrying to send an activity. +/// +/// This struct encapsulates all the necessary information and resources for attempting +/// to send an activity to multiple inbox URLs, with built-in retry logic. +pub(crate) struct SendRetryTask<'a> { + pub activity: &'a SentActivity, + pub object: &'a SharedInboxActivities, + /// Must not be empty at this point + pub inbox_urls: Vec, + /// Channel to report results back to the main instance worker + pub report: &'a mut UnboundedSender, + /// The first request will be sent immediately, but subsequent requests will be delayed + /// according to the number of previous fails + 1 + /// + /// This is a read-only immutable variable that is passed only one way, from the main + /// thread to each send task. It allows the task to determine how long to sleep initially + /// if the request fails. + pub initial_fail_count: i32, + /// For logging purposes + pub domain: String, + pub context: Data, + pub stop: CancellationToken, +} + +impl<'a> SendRetryTask<'a> { + // this function will return successfully when (a) send succeeded or (b) worker cancelled + // and will return an error if an internal error occurred (send errors cause an infinite loop) + pub async fn send_retry_loop(self) -> Result<()> { + let SendRetryTask { + activity, + object, + inbox_urls, + report, + initial_fail_count, + domain, + context, + stop, + } = self; + debug_assert!(!inbox_urls.is_empty()); + + let pool = &mut context.pool(); + let Some(actor_apub_id) = &activity.actor_apub_id else { + return Err(anyhow::anyhow!("activity is from before lemmy 0.19")); + }; + let actor = get_actor_cached(pool, activity.actor_type, actor_apub_id) + .await + .context("failed getting actor instance (was it marked deleted / removed?)")?; + + let object = WithContext::new(object.clone(), FEDERATION_CONTEXT.deref().clone()); + let requests = SendActivityTask::prepare(&object, actor.as_ref(), inbox_urls, &context).await?; + for task in requests { + // usually only one due to shared inbox + tracing::debug!("sending out {}", task); + let mut fail_count = initial_fail_count; + while let Err(e) = task.sign_and_send(&context).await { + fail_count += 1; + report.send(SendActivityResult::Failure { + fail_count, + // activity_id: activity.id, + })?; + let retry_delay = federate_retry_sleep_duration(fail_count); + tracing::info!( + "{}: retrying {:?} attempt {} with delay {retry_delay:.2?}. ({e})", + domain, + activity.id, + fail_count + ); + tokio::select! { + () = sleep(retry_delay) => {}, + () = stop.cancelled() => { + // cancel sending without reporting any result. + // the InstanceWorker needs to be careful to not hang on receive of that + // channel when cancelled (see handle_send_results) + return Ok(()); + } + } + } + } + report.send(SendActivityResult::Success(SendSuccessInfo { + activity_id: activity.id, + published: Some(activity.published), + was_skipped: false, + }))?; + Ok(()) + } +} diff --git a/crates/federate/src/util.rs b/crates/federate/src/util.rs index 60361c3c9..afbe957a5 100644 --- a/crates/federate/src/util.rs +++ b/crates/federate/src/util.rs @@ -1,7 +1,6 @@ use anyhow::{anyhow, Context, Result}; use diesel::prelude::*; use diesel_async::RunQueryDsl; -use lemmy_api_common::lemmy_utils::CACHE_DURATION_FEDERATION; use lemmy_apub::{ activity_lists::SharedInboxActivities, fetcher::{site_or_community_or_user::SiteOrCommunityOrUser, user_or_community::UserOrCommunity}, @@ -28,19 +27,28 @@ use tokio_util::sync::CancellationToken; /// Decrease the delays of the federation queue. /// Should only be used for federation tests since it significantly increases CPU and DB load of the -/// federation queue. +/// federation queue. This is intentionally a separate flag from other flags like debug_assertions, +/// since this is a invasive change we only need rarely. pub(crate) static LEMMY_TEST_FAST_FEDERATION: Lazy = Lazy::new(|| { std::env::var("LEMMY_TEST_FAST_FEDERATION") .map(|s| !s.is_empty()) .unwrap_or(false) }); -/// Recheck for new federation work every n seconds. +/// Recheck for new federation work every n seconds within each InstanceWorker. /// /// When the queue is processed faster than new activities are added and it reaches the current time /// with an empty batch, this is the delay the queue waits before it checks if new activities have /// been added to the sent_activities table. This delay is only applied if no federated activity -/// happens during sending activities of the last batch. +/// happens during sending activities of the last batch, which means on high-activity instances it +/// may never be used. This means that it does not affect the maximum throughput of the queue. +/// +/// +/// This is thus the interval with which tokio wakes up each of the +/// InstanceWorkers to check for new work, if the queue previously was empty. +/// If the delay is too short, the workers (one per federated instance) will wake up too +/// often and consume a lot of CPU. If the delay is long, then activities on low-traffic instances +/// will on average take delay/2 seconds to federate. pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy = Lazy::new(|| { if *LEMMY_TEST_FAST_FEDERATION { Duration::from_millis(100) @@ -49,6 +57,21 @@ pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy = Lazy::new(|| { } }); +/// Cache the latest activity id for a certain duration. +/// +/// This cache is common to all the instance workers and prevents there from being more than one +/// call per N seconds between each DB query to find max(activity_id). +pub(crate) static CACHE_DURATION_LATEST_ID: Lazy = Lazy::new(|| { + if *LEMMY_TEST_FAST_FEDERATION { + // in test mode, we use the same cache duration as the recheck delay so when recheck happens + // data is fresh, accelerating the time the tests take. + *WORK_FINISHED_RECHECK_DELAY + } else { + // in normal mode, we limit the query to one per second + Duration::from_secs(1) + } +}); + /// A task that will be run in an infinite loop, unless it is cancelled. /// If the task exits without being cancelled, an error will be logged and the task will be /// restarted. @@ -174,7 +197,7 @@ pub(crate) async fn get_activity_cached( pub(crate) async fn get_latest_activity_id(pool: &mut DbPool<'_>) -> Result { static CACHE: Lazy> = Lazy::new(|| { Cache::builder() - .time_to_live(CACHE_DURATION_FEDERATION) + .time_to_live(*CACHE_DURATION_LATEST_ID) .build() }); CACHE diff --git a/crates/federate/src/worker.rs b/crates/federate/src/worker.rs index 25c9278aa..f6e70d846 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -1,132 +1,192 @@ -use crate::util::{ - get_activity_cached, - get_actor_cached, - get_latest_activity_id, - FederationQueueStateWithDomain, - LEMMY_TEST_FAST_FEDERATION, - WORK_FINISHED_RECHECK_DELAY, -}; -use activitypub_federation::{ - activity_sending::SendActivityTask, - config::Data, - protocol::context::WithContext, +use crate::{ + inboxes::RealCommunityInboxCollector, + send::{SendActivityResult, SendRetryTask, SendSuccessInfo}, + util::{ + get_activity_cached, + get_latest_activity_id, + FederationQueueStateWithDomain, + WORK_FINISHED_RECHECK_DELAY, + }, }; +use activitypub_federation::config::FederationConfig; use anyhow::{Context, Result}; use chrono::{DateTime, Days, TimeZone, Utc}; -use lemmy_api_common::{context::LemmyContext, federate_retry_sleep_duration}; -use lemmy_apub::{activity_lists::SharedInboxActivities, FEDERATION_CONTEXT}; +use lemmy_api_common::{ + context::LemmyContext, + federate_retry_sleep_duration, + lemmy_utils::settings::structs::FederationWorkerConfig, +}; use lemmy_db_schema::{ - newtypes::{ActivityId, CommunityId, InstanceId}, + newtypes::ActivityId, source::{ - activity::SentActivity, federation_queue_state::FederationQueueState, instance::{Instance, InstanceForm}, - site::Site, }, - utils::naive_now, + utils::{naive_now, ActualDbPool, DbPool}, }; -use lemmy_db_views_actor::structs::CommunityFollowerView; -use once_cell::sync::Lazy; -use reqwest::Url; -use std::{ - collections::{HashMap, HashSet}, - ops::{Add, Deref}, - time::Duration, +use std::{collections::BinaryHeap, ops::Add, time::Duration}; +use tokio::{ + sync::mpsc::{self, UnboundedSender}, + time::sleep, }; -use tokio::{sync::mpsc::UnboundedSender, time::sleep}; use tokio_util::sync::CancellationToken; -use tracing::{debug, info, trace, warn}; -/// Check whether to save state to db every n sends if there's no failures (during failures state is -/// saved after every attempt). This determines the batch size for loop_batch. After a batch ends -/// and SAVE_STATE_EVERY_TIME has passed, the federation_queue_state is updated in the DB. -static CHECK_SAVE_STATE_EVERY_IT: i64 = 100; /// Save state to db after this time has passed since the last state (so if the server crashes or is /// SIGKILLed, less than X seconds of activities are resent) +#[cfg(not(test))] static SAVE_STATE_EVERY_TIME: Duration = Duration::from_secs(60); -/// interval with which new additions to community_followers are queried. +#[cfg(test)] +/// in test mode, we want it to save state and send it to print_stats after every send +static SAVE_STATE_EVERY_TIME: Duration = Duration::from_secs(0); +/// Maximum number of successful sends to allow out of order +const MAX_SUCCESSFULS: usize = 1000; + +/// in prod mode, try to collect multiple send results at the same time to reduce load +#[cfg(not(test))] +static MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE: usize = 4; +#[cfg(test)] +static MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE: usize = 0; + /// -/// The first time some user on an instance follows a specific remote community (or, more precisely: -/// the first time a (followed_community_id, follower_inbox_url) tuple appears), this delay limits -/// the maximum time until the follow actually results in activities from that community id being -/// sent to that inbox url. This delay currently needs to not be too small because the DB load is -/// currently fairly high because of the current structure of storing inboxes for every person, not -/// having a separate list of shared_inboxes, and the architecture of having every instance queue be -/// fully separate. (see https://github.com/LemmyNet/lemmy/issues/3958) -static FOLLOW_ADDITIONS_RECHECK_DELAY: Lazy = Lazy::new(|| { - if *LEMMY_TEST_FAST_FEDERATION { - chrono::TimeDelta::try_seconds(1).expect("TimeDelta out of bounds") - } else { - chrono::TimeDelta::try_minutes(2).expect("TimeDelta out of bounds") - } -}); -/// The same as FOLLOW_ADDITIONS_RECHECK_DELAY, but triggering when the last person on an instance -/// unfollows a specific remote community. This is expected to happen pretty rarely and updating it -/// in a timely manner is not too important. -static FOLLOW_REMOVALS_RECHECK_DELAY: Lazy = - Lazy::new(|| chrono::TimeDelta::try_hours(1).expect("TimeDelta out of bounds")); +/// SendManager --(has many)--> InstanceWorker --(has many)--> SendRetryTask +/// | | | +/// -----|------create worker -> loop activities--create task-> send activity +/// | | vvvv +/// | | fail or success +/// | | <-report result-- | +/// | <---order and aggrate results--- | +/// | <---send stats--- | | +/// filter and print stats | | pub(crate) struct InstanceWorker { instance: Instance, - // load site lazily because if an instance is first seen due to being on allowlist, - // the corresponding row in `site` may not exist yet since that is only added once - // `fetch_instance_actor_for_object` is called. - // (this should be unlikely to be relevant outside of the federation tests) - site_loaded: bool, - site: Option, - followed_communities: HashMap>, stop: CancellationToken, - context: Data, - stats_sender: UnboundedSender, - last_full_communities_fetch: DateTime, - last_incremental_communities_fetch: DateTime, + federation_lib_config: FederationConfig, + federation_worker_config: FederationWorkerConfig, state: FederationQueueState, last_state_insert: DateTime, + pool: ActualDbPool, + inbox_collector: RealCommunityInboxCollector, + // regularily send stats back to the SendManager + stats_sender: UnboundedSender, + // each HTTP send will report back to this channel concurrently + receive_send_result: mpsc::UnboundedReceiver, + // this part of the channel is cloned and passed to the SendRetryTasks + report_send_result: mpsc::UnboundedSender, + // activities that have been successfully sent but + // that are not the lowest number and thus can't be written to the database yet + successfuls: BinaryHeap, + // number of activities that currently have a task spawned to send it + in_flight: i8, } impl InstanceWorker { pub(crate) async fn init_and_loop( instance: Instance, - context: Data, + config: FederationConfig, + federation_worker_config: FederationWorkerConfig, stop: CancellationToken, stats_sender: UnboundedSender, ) -> Result<(), anyhow::Error> { - let mut pool = context.pool(); - let state = FederationQueueState::load(&mut pool, instance.id).await?; + let pool = config.to_request_data().inner_pool().clone(); + let state = FederationQueueState::load(&mut DbPool::Pool(&pool), instance.id).await?; + let (report_send_result, receive_send_result) = + tokio::sync::mpsc::unbounded_channel::(); let mut worker = InstanceWorker { + inbox_collector: RealCommunityInboxCollector::new_real( + pool.clone(), + instance.id, + instance.domain.clone(), + ), + federation_worker_config, instance, - site_loaded: false, - site: None, - followed_communities: HashMap::new(), stop, - context, + federation_lib_config: config, stats_sender, - last_full_communities_fetch: Utc.timestamp_nanos(0), - last_incremental_communities_fetch: Utc.timestamp_nanos(0), state, last_state_insert: Utc.timestamp_nanos(0), + pool, + receive_send_result, + report_send_result, + successfuls: BinaryHeap::::new(), + in_flight: 0, }; + worker.loop_until_stopped().await } /// loop fetch new activities from db and send them to the inboxes of the given instances /// this worker only returns if (a) there is an internal error or (b) the cancellation token is /// cancelled (graceful exit) - pub(crate) async fn loop_until_stopped(&mut self) -> Result<(), anyhow::Error> { - debug!("Starting federation worker for {}", self.instance.domain); - let save_state_every = chrono::Duration::from_std(SAVE_STATE_EVERY_TIME).expect("not negative"); - - self.update_communities().await?; + async fn loop_until_stopped(&mut self) -> Result<()> { self.initial_fail_sleep().await?; + let (mut last_sent_id, mut newest_id) = self.get_latest_ids().await?; + while !self.stop.is_cancelled() { - self.loop_batch().await?; - if self.stop.is_cancelled() { - break; + // check if we need to wait for a send to finish before sending the next one + // we wait if (a) the last request failed, only if a request is already in flight (not at the + // start of the loop) or (b) if we have too many successfuls in memory or (c) if we have + // too many in flight + let need_wait_for_event = (self.in_flight != 0 && self.state.fail_count > 0) + || self.successfuls.len() >= MAX_SUCCESSFULS + || self.in_flight >= self.federation_worker_config.concurrent_sends_per_instance; + if need_wait_for_event || self.receive_send_result.len() > MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE + { + // if len() > 0 then this does not block and allows us to write to db more often + // if len is 0 then this means we wait for something to change our above conditions, + // which can only happen by an event sent into the channel + self.handle_send_results().await?; + // handle_send_results does not guarantee that we are now in a condition where we want to + // send a new one, so repeat this check until the if no longer applies + continue; } - if (Utc::now() - self.last_state_insert) > save_state_every { - self.save_and_send_state().await?; + + // send a new activity if there is one + self.inbox_collector.update_communities().await?; + let next_id_to_send = ActivityId(last_sent_id.0 + 1); + { + // sanity check: calculate next id to send based on the last id and the in flight requests + let expected_next_id = self.state.last_successful_id.map(|last_successful_id| { + last_successful_id.0 + (self.successfuls.len() as i64) + i64::from(self.in_flight) + 1 + }); + // compare to next id based on incrementing + if expected_next_id != Some(next_id_to_send.0) { + anyhow::bail!( + "{}: next id to send is not as expected: {:?} != {:?}", + self.instance.domain, + expected_next_id, + next_id_to_send + ) + } } - self.update_communities().await?; + + if next_id_to_send > newest_id { + // lazily fetch latest id only if we have cought up + newest_id = self.get_latest_ids().await?.1; + if next_id_to_send > newest_id { + if next_id_to_send > ActivityId(newest_id.0 + 1) { + tracing::error!( + "{}: next send id {} is higher than latest id {}+1 in database (did the db get cleared?)", + self.instance.domain, + next_id_to_send.0, + newest_id.0 + ); + } + // no more work to be done, wait before rechecking + tokio::select! { + () = sleep(*WORK_FINISHED_RECHECK_DELAY) => {}, + () = self.stop.cancelled() => { + tracing::debug!("cancelled worker loop while waiting for new work") + } + } + continue; + } + } + self.in_flight += 1; + last_sent_id = next_id_to_send; + self.spawn_send_if_needed(next_id_to_send).await?; } - // final update of state in db + tracing::debug!("cancelled worker loop after send"); + + // final update of state in db on shutdown self.save_and_send_state().await?; Ok(()) } @@ -144,18 +204,27 @@ impl InstanceWorker { return Ok(()); } let remaining = required - elapsed; + tracing::debug!( + "{}: fail-sleeping for {:?} before starting queue", + self.instance.domain, + remaining + ); tokio::select! { () = sleep(remaining) => {}, - () = self.stop.cancelled() => {} + () = self.stop.cancelled() => { + tracing::debug!("cancelled worker loop during initial fail sleep") + } } } Ok(()) } - /// send out a batch of CHECK_SAVE_STATE_EVERY_IT activities - async fn loop_batch(&mut self) -> Result<()> { - let latest_id = get_latest_activity_id(&mut self.context.pool()).await?; - let mut id = if let Some(id) = self.state.last_successful_id { - id + + /// return the last successfully sent id and the newest activity id in the database + /// sets last_successful_id in database if it's the first time this instance is seen + async fn get_latest_ids(&mut self) -> Result<(ActivityId, ActivityId)> { + let latest_id = get_latest_activity_id(&mut self.pool()).await?; + let last = if let Some(last) = self.state.last_successful_id { + last } else { // this is the initial creation (instance first seen) of the federation queue for this // instance @@ -166,203 +235,542 @@ impl InstanceWorker { self.save_and_send_state().await?; latest_id }; - if id >= latest_id { - if id > latest_id { - tracing::error!( - "{}: last successful id {} is higher than latest id {} in database (did the db get cleared?)", - self.instance.domain, - id.0, - latest_id.0 - ); - } - // no more work to be done, wait before rechecking - tokio::select! { - () = sleep(*WORK_FINISHED_RECHECK_DELAY) => {}, - () = self.stop.cancelled() => {} - } - return Ok(()); - } - let mut processed_activities = 0; - while id < latest_id - && processed_activities < CHECK_SAVE_STATE_EVERY_IT - && !self.stop.is_cancelled() - { - id = ActivityId(id.0 + 1); - processed_activities += 1; - let Some(ele) = get_activity_cached(&mut self.context.pool(), id) - .await - .context("failed reading activity from db")? - else { - debug!("{}: {:?} does not exist", self.instance.domain, id); - self.state.last_successful_id = Some(id); - continue; - }; - if let Err(e) = self.send_retry_loop(&ele.0, &ele.1).await { - warn!( - "sending {} errored internally, skipping activity: {:?}", - ele.0.ap_id, e - ); - } - if self.stop.is_cancelled() { + Ok((last, latest_id)) + } + + async fn handle_send_results(&mut self) -> Result<(), anyhow::Error> { + let mut force_write = false; + let mut events = Vec::new(); + // Wait for at least one event but if there's multiple handle them all. + // We need to listen to the cancel event here as well in order to prevent a hang on shutdown: + // If the SendRetryTask gets cancelled, it immediately exits without reporting any state. + // So if the worker is waiting for a send result and all SendRetryTask gets cancelled, this recv + // could hang indefinitely otherwise. The tasks will also drop their handle of + // report_send_result which would cause the recv_many method to return 0 elements, but since + // InstanceWorker holds a copy of the send result channel as well, that won't happen. + tokio::select! { + _ = self.receive_send_result.recv_many(&mut events, 1000) => {}, + () = self.stop.cancelled() => { + tracing::debug!("cancelled worker loop while waiting for send results"); return Ok(()); } - // send success! - self.state.last_successful_id = Some(id); - self.state.last_successful_published_time = Some(ele.0.published); - self.state.fail_count = 0; + } + for event in events { + match event { + SendActivityResult::Success(s) => { + self.in_flight -= 1; + if !s.was_skipped { + self.state.fail_count = 0; + self.mark_instance_alive().await?; + } + self.successfuls.push(s); + } + SendActivityResult::Failure { fail_count, .. } => { + if fail_count > self.state.fail_count { + // override fail count - if multiple activities are currently sending this value may get + // conflicting info but that's fine. + // This needs to be this way, all alternatives would be worse. The reason is that if 10 + // simultaneous requests fail within a 1s period, we don't want the next retry to be + // exponentially 2**10 s later. Any amount of failures within a fail-sleep period should + // only count as one failure. + + self.state.fail_count = fail_count; + self.state.last_retry = Some(Utc::now()); + force_write = true; + } + } + } + } + self.pop_successfuls_and_write(force_write).await?; + Ok(()) + } + async fn mark_instance_alive(&mut self) -> Result<()> { + // Activity send successful, mark instance as alive if it hasn't been updated in a while. + let updated = self.instance.updated.unwrap_or(self.instance.published); + if updated.add(Days::new(1)) < Utc::now() { + self.instance.updated = Some(Utc::now()); + + let form = InstanceForm::builder() + .domain(self.instance.domain.clone()) + .updated(Some(naive_now())) + .build(); + Instance::update(&mut self.pool(), self.instance.id, form).await?; + } + Ok(()) + } + /// Checks that sequential activities `last_successful_id + 1`, `last_successful_id + 2` etc have + /// been sent successfully. In that case updates `last_successful_id` and saves the state to the + /// database if the time since the last save is greater than `SAVE_STATE_EVERY_TIME`. + async fn pop_successfuls_and_write(&mut self, force_write: bool) -> Result<()> { + let Some(mut last_id) = self.state.last_successful_id else { + tracing::warn!( + "{} should be impossible: last successful id is None", + self.instance.domain + ); + return Ok(()); + }; + tracing::debug!( + "{} last: {:?}, next: {:?}, currently in successfuls: {:?}", + self.instance.domain, + last_id, + self.successfuls.peek(), + self.successfuls.iter() + ); + while self + .successfuls + .peek() + .map(|a| a.activity_id == ActivityId(last_id.0 + 1)) + .unwrap_or(false) + { + let next = self + .successfuls + .pop() + .context("peek above ensures pop has value")?; + last_id = next.activity_id; + self.state.last_successful_id = Some(next.activity_id); + self.state.last_successful_published_time = next.published; + } + + let save_state_every = chrono::Duration::from_std(SAVE_STATE_EVERY_TIME).expect("not negative"); + if force_write || (Utc::now() - self.last_state_insert) > save_state_every { + self.save_and_send_state().await?; } Ok(()) } - // this function will return successfully when (a) send succeeded or (b) worker cancelled - // and will return an error if an internal error occurred (send errors cause an infinite loop) - async fn send_retry_loop( - &mut self, - activity: &SentActivity, - object: &SharedInboxActivities, - ) -> Result<()> { + /// we collect the relevant inboxes in the main instance worker task, and only spawn the send task + /// if we have inboxes to send to this limits CPU usage and reduces overhead for the (many) + /// cases where we don't have any inboxes + async fn spawn_send_if_needed(&mut self, activity_id: ActivityId) -> Result<()> { + let Some(ele) = get_activity_cached(&mut self.pool(), activity_id) + .await + .context("failed reading activity from db")? + else { + tracing::debug!("{}: {:?} does not exist", self.instance.domain, activity_id); + self + .report_send_result + .send(SendActivityResult::Success(SendSuccessInfo { + activity_id, + published: None, + was_skipped: true, + }))?; + return Ok(()); + }; + let activity = &ele.0; let inbox_urls = self + .inbox_collector .get_inbox_urls(activity) .await .context("failed figuring out inbox urls")?; if inbox_urls.is_empty() { - trace!("{}: {:?} no inboxes", self.instance.domain, activity.id); - self.state.last_successful_id = Some(activity.id); - self.state.last_successful_published_time = Some(activity.published); + // this is the case when the activity is not relevant to this receiving instance (e.g. no user + // subscribed to the relevant community) + tracing::debug!("{}: {:?} no inboxes", self.instance.domain, activity.id); + self + .report_send_result + .send(SendActivityResult::Success(SendSuccessInfo { + activity_id, + // it would be valid here to either return None or Some(activity.published). The published + // time is only used for stats pages that track federation delay. None can be a bit + // misleading because if you look at / chart the published time for federation from a + // large to a small instance that's only subscribed to a few small communities, + // then it will show the last published time as a days ago even though + // federation is up to date. + published: Some(activity.published), + was_skipped: true, + }))?; return Ok(()); } - let Some(actor_apub_id) = &activity.actor_apub_id else { - return Ok(()); // activity was inserted before persistent queue was activated - }; - let actor = get_actor_cached(&mut self.context.pool(), activity.actor_type, actor_apub_id) - .await - .context("failed getting actor instance (was it marked deleted / removed?)")?; - - let object = WithContext::new(object.clone(), FEDERATION_CONTEXT.deref().clone()); - let inbox_urls = inbox_urls.into_iter().collect(); - let requests = - SendActivityTask::prepare(&object, actor.as_ref(), inbox_urls, &self.context).await?; - for task in requests { - // usually only one due to shared inbox - trace!("sending out {}", task); - while let Err(e) = task.sign_and_send(&self.context).await { - self.state.fail_count += 1; - self.state.last_retry = Some(Utc::now()); - let retry_delay: Duration = federate_retry_sleep_duration(self.state.fail_count); - info!( - "{}: retrying {:?} attempt {} with delay {retry_delay:.2?}. ({e})", - self.instance.domain, activity.id, self.state.fail_count + let initial_fail_count = self.state.fail_count; + let data = self.federation_lib_config.to_request_data(); + let stop = self.stop.clone(); + let domain = self.instance.domain.clone(); + let mut report = self.report_send_result.clone(); + tokio::spawn(async move { + let res = SendRetryTask { + activity: &ele.0, + object: &ele.1, + inbox_urls, + report: &mut report, + initial_fail_count, + domain, + context: data, + stop, + } + .send_retry_loop() + .await; + if let Err(e) = res { + tracing::warn!( + "sending {} errored internally, skipping activity: {:?}", + ele.0.ap_id, + e ); - self.save_and_send_state().await?; - tokio::select! { - () = sleep(retry_delay) => {}, - () = self.stop.cancelled() => { - // save state to db and exit - return Ok(()); - } - } + // An error in this location means there is some deeper internal issue with the activity, + // for example the actor can't be loaded or similar. These issues are probably not + // solveable by retrying and would cause the federation for this instance to permanently be + // stuck in a retry loop. So we log the error and skip the activity (by reporting success to + // the worker) + report + .send(SendActivityResult::Success(SendSuccessInfo { + activity_id, + published: None, + was_skipped: true, + })) + .ok(); } - - // Activity send successful, mark instance as alive if it hasn't been updated in a while. - let updated = self.instance.updated.unwrap_or(self.instance.published); - if updated.add(Days::new(1)) < Utc::now() { - self.instance.updated = Some(Utc::now()); - - let form = InstanceForm::builder() - .domain(self.instance.domain.clone()) - .updated(Some(naive_now())) - .build(); - Instance::update(&mut self.context.pool(), self.instance.id, form).await?; - } - } + }); Ok(()) } - /// get inbox urls of sending the given activity to the given instance - /// most often this will return 0 values (if instance doesn't care about the activity) - /// or 1 value (the shared inbox) - /// > 1 values only happens for non-lemmy software - async fn get_inbox_urls(&mut self, activity: &SentActivity) -> Result> { - let mut inbox_urls: HashSet = HashSet::new(); - - if activity.send_all_instances { - if !self.site_loaded { - self.site = Site::read_from_instance_id(&mut self.context.pool(), self.instance.id).await?; - self.site_loaded = true; - } - if let Some(site) = &self.site { - // Nutomic: Most non-lemmy software wont have a site row. That means it cant handle these - // activities. So handling it like this is fine. - inbox_urls.insert(site.inbox_url.inner().clone()); - } - } - if let Some(t) = &activity.send_community_followers_of { - if let Some(urls) = self.followed_communities.get(t) { - inbox_urls.extend(urls.iter().cloned()); - } - } - inbox_urls.extend( - activity - .send_inboxes - .iter() - .filter_map(std::option::Option::as_ref) - .filter(|&u| (u.domain() == Some(&self.instance.domain))) - .map(|u| u.inner().clone()), - ); - Ok(inbox_urls) - } - - async fn update_communities(&mut self) -> Result<()> { - if (Utc::now() - self.last_full_communities_fetch) > *FOLLOW_REMOVALS_RECHECK_DELAY { - // process removals every hour - (self.followed_communities, self.last_full_communities_fetch) = self - .get_communities(self.instance.id, Utc.timestamp_nanos(0)) - .await?; - self.last_incremental_communities_fetch = self.last_full_communities_fetch; - } - if (Utc::now() - self.last_incremental_communities_fetch) > *FOLLOW_ADDITIONS_RECHECK_DELAY { - // process additions every minute - let (news, time) = self - .get_communities(self.instance.id, self.last_incremental_communities_fetch) - .await?; - self.followed_communities.extend(news); - self.last_incremental_communities_fetch = time; - } - Ok(()) - } - - /// get a list of local communities with the remote inboxes on the given instance that cares about - /// them - async fn get_communities( - &mut self, - instance_id: InstanceId, - last_fetch: DateTime, - ) -> Result<(HashMap>, DateTime)> { - let new_last_fetch = - Utc::now() - chrono::TimeDelta::try_seconds(10).expect("TimeDelta out of bounds"); // update to time before fetch to ensure overlap. subtract 10s to ensure overlap even if - // published date is not exact - Ok(( - CommunityFollowerView::get_instance_followed_community_inboxes( - &mut self.context.pool(), - instance_id, - last_fetch, - ) - .await? - .into_iter() - .fold(HashMap::new(), |mut map, (c, u)| { - map.entry(c).or_default().insert(u.into()); - map - }), - new_last_fetch, - )) - } async fn save_and_send_state(&mut self) -> Result<()> { + tracing::debug!("{}: saving and sending state", self.instance.domain); self.last_state_insert = Utc::now(); - FederationQueueState::upsert(&mut self.context.pool(), &self.state).await?; + FederationQueueState::upsert(&mut self.pool(), &self.state).await?; self.stats_sender.send(FederationQueueStateWithDomain { state: self.state.clone(), domain: self.instance.domain.clone(), })?; Ok(()) } + + fn pool(&self) -> DbPool<'_> { + DbPool::Pool(&self.pool) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +#[allow(clippy::indexing_slicing)] +mod test { + + use super::*; + use activitypub_federation::{ + http_signatures::generate_actor_keypair, + 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_db_schema::{ + newtypes::DbUrl, + source::{ + activity::{ActorType, SentActivity, SentActivityForm}, + person::{Person, PersonInsertForm}, + }, + traits::Crud, + }; + use lemmy_utils::error::LemmyResult; + use reqwest::StatusCode; + use serde_json::{json, Value}; + use serial_test::serial; + use test_context::{test_context, AsyncTestContext}; + use tokio::{ + spawn, + sync::mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver}, + }; + use tracing_test::traced_test; + use url::Url; + + struct Data { + context: activitypub_federation::config::Data, + instance: Instance, + person: Person, + stats_receiver: UnboundedReceiver, + inbox_receiver: UnboundedReceiver, + cancel: CancellationToken, + cleaned_up: bool, + wait_stop_server: ServerHandle, + is_concurrent: bool, + } + + impl Data { + async fn init() -> LemmyResult { + let context = LemmyContext::init_test_federation_config().await; + let instance = Instance::read_or_create(&mut context.pool(), "localhost".to_string()).await?; + + let actor_keypair = generate_actor_keypair()?; + let actor_id: DbUrl = Url::parse("http://local.com/u/alice")?.into(); + let 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())?), + ..PersonInsertForm::new("alice".to_string(), actor_keypair.public_key, instance.id) + }; + let person = Person::create(&mut context.pool(), &person_form).await?; + + let cancel = CancellationToken::new(); + let (stats_sender, stats_receiver) = unbounded_channel(); + let (inbox_sender, inbox_receiver) = unbounded_channel(); + + // listen for received activities in background + let wait_stop_server = listen_activities(inbox_sender)?; + + let concurrent_sends_per_instance = std::env::var("LEMMY_TEST_FEDERATION_CONCURRENT_SENDS") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(10); + + let fed_config = FederationWorkerConfig { + concurrent_sends_per_instance, + }; + spawn(InstanceWorker::init_and_loop( + instance.clone(), + context.clone(), + fed_config, + cancel.clone(), + stats_sender, + )); + // wait for startup + sleep(*WORK_FINISHED_RECHECK_DELAY).await; + + Ok(Self { + context: context.to_request_data(), + instance, + person, + stats_receiver, + inbox_receiver, + cancel, + wait_stop_server, + cleaned_up: false, + is_concurrent: concurrent_sends_per_instance > 1, + }) + } + + async fn cleanup(&mut self) -> LemmyResult<()> { + if self.cleaned_up { + return Ok(()); + } + self.cleaned_up = true; + self.cancel.cancel(); + sleep(*WORK_FINISHED_RECHECK_DELAY).await; + Instance::delete_all(&mut self.context.pool()).await?; + Person::delete(&mut self.context.pool(), self.person.id).await?; + self.wait_stop_server.stop(true).await; + Ok(()) + } + } + + /// In order to guarantee that the webserver is stopped via the cleanup function, + /// we implement a test context. + impl AsyncTestContext for Data { + async fn setup() -> Data { + Data::init().await.unwrap() + } + async fn teardown(mut self) { + self.cleanup().await.unwrap() + } + } + + #[test_context(Data)] + #[tokio::test] + #[traced_test] + #[serial] + async fn test_stats(data: &mut Data) -> LemmyResult<()> { + tracing::debug!("hello world"); + + // first receive at startup + let rcv = data.stats_receiver.recv().await.unwrap(); + tracing::debug!("received first stats"); + assert_eq!(data.instance.id, rcv.state.instance_id); + + let sent = send_activity(data.person.actor_id.clone(), &data.context, true).await?; + tracing::debug!("sent activity"); + // receive for successfully sent activity + let inbox_rcv = data.inbox_receiver.recv().await.unwrap(); + let parsed_activity = serde_json::from_str::>(&inbox_rcv)?; + assert_eq!(&sent.data, parsed_activity.inner()); + tracing::debug!("received activity"); + + let rcv = data.stats_receiver.recv().await.unwrap(); + assert_eq!(data.instance.id, rcv.state.instance_id); + assert_eq!(Some(sent.id), rcv.state.last_successful_id); + tracing::debug!("received second stats"); + + data.cleanup().await?; + + // it also sends state on shutdown + let rcv = data.stats_receiver.try_recv(); + assert!(rcv.is_ok()); + + // nothing further received + let rcv = data.stats_receiver.try_recv(); + assert_eq!(Some(TryRecvError::Disconnected), rcv.err()); + let inbox_rcv = data.inbox_receiver.try_recv(); + assert_eq!(Some(TryRecvError::Disconnected), inbox_rcv.err()); + + Ok(()) + } + + #[test_context(Data)] + #[tokio::test] + #[traced_test] + #[serial] + async fn test_send_40(data: &mut Data) -> LemmyResult<()> { + tracing::debug!("hello world"); + + // first receive at startup + let rcv = data.stats_receiver.recv().await.unwrap(); + tracing::debug!("received first stats"); + assert_eq!(data.instance.id, rcv.state.instance_id); + // assert_eq!(Some(ActivityId(0)), rcv.state.last_successful_id); + // let last_id_before = rcv.state.last_successful_id.unwrap(); + let mut sent = Vec::new(); + for _ in 0..40 { + sent.push(send_activity(data.person.actor_id.clone(), &data.context, false).await?); + } + sleep(2 * *WORK_FINISHED_RECHECK_DELAY).await; + tracing::debug!("sent activity"); + compare_sent_with_receive(data, sent).await?; + + Ok(()) + } + + #[test_context(Data)] + #[tokio::test] + #[traced_test] + #[serial] + /// this test sends 15 activities, waits and checks they have all been received, then sends 50, + /// etc + async fn test_send_15_20_30(data: &mut Data) -> LemmyResult<()> { + tracing::debug!("hello world"); + + // first receive at startup + let rcv = data.stats_receiver.recv().await.unwrap(); + tracing::debug!("received first stats"); + assert_eq!(data.instance.id, rcv.state.instance_id); + // assert_eq!(Some(ActivityId(0)), rcv.state.last_successful_id); + // let last_id_before = rcv.state.last_successful_id.unwrap(); + let counts = vec![15, 20, 35]; + for count in counts { + tracing::debug!("sending {} activities", count); + let mut sent = Vec::new(); + for _ in 0..count { + sent.push(send_activity(data.person.actor_id.clone(), &data.context, false).await?); + } + sleep(2 * *WORK_FINISHED_RECHECK_DELAY).await; + tracing::debug!("sent activity"); + compare_sent_with_receive(data, sent).await?; + } + + Ok(()) + } + + #[test_context(Data)] + #[tokio::test] + #[serial] + async fn test_update_instance(data: &mut Data) -> LemmyResult<()> { + let form = InstanceForm::builder() + .domain(data.instance.domain.clone()) + .updated(None) + .build(); + Instance::update(&mut data.context.pool(), data.instance.id, form).await?; + + send_activity(data.person.actor_id.clone(), &data.context, true).await?; + data.inbox_receiver.recv().await.unwrap(); + + let instance = + Instance::read_or_create(&mut data.context.pool(), data.instance.domain.clone()).await?; + + assert!(instance.updated.is_some()); + + data.cleanup().await?; + + Ok(()) + } + + fn listen_activities(inbox_sender: UnboundedSender) -> LemmyResult { + let run = HttpServer::new(move || { + App::new() + .app_data(actix_web::web::Data::new(inbox_sender.clone())) + .route( + "/inbox", + web::post().to( + |inbox_sender: actix_web::web::Data>, body: String| async move { + tracing::debug!("received activity: {:?}", body); + inbox_sender.send(body.clone()).unwrap(); + HttpResponse::new(StatusCode::OK) + }, + ), + ) + }) + .bind(("127.0.0.1", 8085))? + .run(); + let handle = run.handle(); + tokio::spawn(async move { + run.await.unwrap(); + /*select! { + _ = run => {}, + _ = cancel.cancelled() => { } + }*/ + }); + Ok(handle) + } + + async fn send_activity( + actor_id: DbUrl, + context: &LemmyContext, + wait: bool, + ) -> LemmyResult { + // create outgoing activity + let data = json!({ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "object": "http://ds9.lemmy.ml/comment/1", + "audience": "https://enterprise.lemmy.ml/c/tenforward", + "type": "Like", + "id": format!("http://ds9.lemmy.ml/activities/like/{}", uuid::Uuid::new_v4()), + }); + let form = SentActivityForm { + ap_id: Url::parse(&format!( + "http://local.com/activity/{}", + uuid::Uuid::new_v4() + ))? + .into(), + data, + sensitive: false, + send_inboxes: vec![Some(Url::parse("http://localhost:8085/inbox")?.into())], + send_all_instances: false, + send_community_followers_of: None, + actor_type: ActorType::Person, + actor_apub_id: actor_id, + }; + let sent = SentActivity::create(&mut context.pool(), form).await?; + + if wait { + sleep(*WORK_FINISHED_RECHECK_DELAY * 2).await; + } + + Ok(sent) + } + async fn compare_sent_with_receive(data: &mut Data, mut sent: Vec) -> Result<()> { + let check_order = !data.is_concurrent; // allow out-of order receiving when running parallel + let mut received = Vec::new(); + for _ in 0..sent.len() { + let inbox_rcv = data.inbox_receiver.recv().await.unwrap(); + let parsed_activity = serde_json::from_str::>(&inbox_rcv)?; + received.push(parsed_activity); + } + if !check_order { + // sort by id + received.sort_by(|a, b| { + a.inner()["id"] + .as_str() + .unwrap() + .cmp(b.inner()["id"].as_str().unwrap()) + }); + sent.sort_by(|a, b| { + a.data["id"] + .as_str() + .unwrap() + .cmp(b.data["id"].as_str().unwrap()) + }); + } + // receive for successfully sent activity + for i in 0..sent.len() { + let sent_activity = &sent[i]; + let received_activity = received[i].inner(); + assert_eq!(&sent_activity.data, received_activity); + tracing::debug!("received activity"); + } + Ok(()) + } } diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 049bd6cc8..10ffb57de 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -41,12 +41,66 @@ pub fn config( .service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete))); } -#[derive(Deserialize)] +trait ProcessUrl { + /// If thumbnail or format is given, this uses the pictrs process endpoint. + /// Otherwise, it uses the normal pictrs url (IE image/original). + fn process_url(&self, image_url: &str, pictrs_url: &Url) -> String; +} + +#[derive(Deserialize, Clone)] struct PictrsGetParams { format: Option, thumbnail: Option, } +impl ProcessUrl for PictrsGetParams { + fn process_url(&self, src: &str, pictrs_url: &Url) -> String { + if self.format.is_none() && self.thumbnail.is_none() { + format!("{}image/original/{}", pictrs_url, src) + } else { + // Take file type from name, or jpg if nothing is given + let format = self + .clone() + .format + .unwrap_or_else(|| src.split('.').last().unwrap_or("jpg").to_string()); + + let mut url = format!("{}image/process.{}?src={}", pictrs_url, format, src); + + if let Some(size) = self.thumbnail { + url = format!("{url}&thumbnail={size}",); + } + url + } + } +} + +#[derive(Deserialize, Clone)] +pub struct ImageProxyParams { + url: String, + format: Option, + thumbnail: Option, +} + +impl ProcessUrl for ImageProxyParams { + fn process_url(&self, proxy_url: &str, pictrs_url: &Url) -> String { + if self.format.is_none() && self.thumbnail.is_none() { + format!("{}image/original?proxy={}", pictrs_url, proxy_url) + } else { + // Take file type from name, or jpg if nothing is given + let format = self + .clone() + .format + .unwrap_or_else(|| proxy_url.split('.').last().unwrap_or("jpg").to_string()); + + let mut url = format!("{}image/process.{}?proxy={}", pictrs_url, format, proxy_url); + + if let Some(size) = self.thumbnail { + url = format!("{url}&thumbnail={size}",); + } + url + } + } +} fn adapt_request( request: &HttpRequest, client: &ClientWithMiddleware, @@ -133,23 +187,10 @@ async fn full_res( // If there are no query params, the URL is original let pictrs_config = context.settings().pictrs_config()?; - let url = if params.format.is_none() && params.thumbnail.is_none() { - format!("{}image/original/{}", pictrs_config.url, name,) - } else { - // Take file type from name, or jpg if nothing is given - let format = params - .format - .unwrap_or_else(|| name.split('.').last().unwrap_or("jpg").to_string()); - let mut url = format!("{}image/process.{}?src={}", pictrs_config.url, format, name,); + let processed_url = params.process_url(name, &pictrs_config.url); - if let Some(size) = params.thumbnail { - url = format!("{url}&thumbnail={size}",); - } - url - }; - - image(url, req, &client).await + image(processed_url, req, &client).await } async fn image( @@ -208,11 +249,6 @@ async fn delete( Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream()))) } -#[derive(Deserialize)] -pub struct ImageProxyParams { - url: String, -} - pub async fn image_proxy( Query(params): Query, req: HttpRequest, @@ -226,9 +262,10 @@ pub async fn image_proxy( RemoteImage::validate(&mut context.pool(), url.clone().into()).await?; let pictrs_config = context.settings().pictrs_config()?; - let url = format!("{}image/original?proxy={}", pictrs_config.url, ¶ms.url); - image(url, req, &client).await + let processed_url = params.process_url(¶ms.url, &pictrs_config.url); + + image(processed_url, req, &client).await } fn make_send(mut stream: S) -> impl Stream + Send + Unpin + 'static diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 046c0a872..1b71b577a 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -44,7 +44,6 @@ full = [ "dep:enum-map", "dep:futures", "dep:tokio", - "dep:openssl", "dep:html2text", "dep:lettre", "dep:uuid", @@ -65,7 +64,6 @@ actix-web = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } reqwest-middleware = { workspace = true, optional = true } strum = { workspace = true } -strum_macros = { workspace = true } futures = { workspace = true, optional = true } diesel = { workspace = true, features = ["chrono"], optional = true } http = { workspace = true, optional = true } @@ -74,13 +72,14 @@ uuid = { workspace = true, features = ["serde", "v4"], optional = true } rosetta-i18n = { workspace = true, optional = true } tokio = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true } -openssl = { version = "0.10.64", optional = true } html2text = { version = "0.12.5", optional = true } deser-hjson = { version = "2.2.4", optional = true } smart-default = { version = "0.7.1", optional = true } -lettre = { version = "0.11.7", features = [ +lettre = { version = "0.11.7", default-features = false, features = [ + "builder", "tokio1", - "tokio1-native-tls", + "tokio1-rustls-tls", + "smtp-transport", ], optional = true } markdown-it = { version = "0.6.0", optional = true } ts-rs = { workspace = true, optional = true } diff --git a/crates/utils/src/apub.rs b/crates/utils/src/apub.rs deleted file mode 100644 index 53e069d77..000000000 --- a/crates/utils/src/apub.rs +++ /dev/null @@ -1,26 +0,0 @@ -use openssl::{pkey::PKey, rsa::Rsa}; -use std::io::{Error, ErrorKind}; - -pub struct Keypair { - pub private_key: String, - pub public_key: String, -} - -/// Generate the asymmetric keypair for ActivityPub HTTP signatures. -pub fn generate_actor_keypair() -> Result { - let rsa = Rsa::generate(2048)?; - let pkey = PKey::from_rsa(rsa)?; - let public_key = pkey.public_key_to_pem()?; - let private_key = pkey.private_key_to_pem_pkcs8()?; - let key_to_string = |key| match String::from_utf8(key) { - Ok(s) => Ok(s), - Err(e) => Err(Error::new( - ErrorKind::Other, - format!("Failed converting key to string: {e}"), - )), - }; - Ok(Keypair { - private_key: key_to_string(private_key)?, - public_key: key_to_string(public_key)?, - }) -} diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 324c08ccb..896539057 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -1,7 +1,7 @@ use cfg_if::cfg_if; use serde::{Deserialize, Serialize}; use std::fmt::Debug; -use strum_macros::{Display, EnumIter}; +use strum::{Display, EnumIter}; #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)] #[cfg_attr(feature = "full", derive(ts_rs::TS))] @@ -38,6 +38,8 @@ pub enum LemmyErrorType { NotTopAdmin, NotTopMod, NotLoggedIn, + NotHigherMod, + NotHigherAdmin, SiteBan, Deleted, BannedFromCommunity, diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 1adb3f6cf..5a5e76d2a 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -2,7 +2,6 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "full")] { - pub mod apub; pub mod cache_header; pub mod email; pub mod rate_limit; diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index 5f1b6f7f5..35c95366d 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -6,7 +6,7 @@ use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, time::Instant, }; -use strum_macros::AsRefStr; +use strum::{AsRefStr, Display}; use tracing::debug; static START_TIME: Lazy = Lazy::new(Instant::now); @@ -66,7 +66,7 @@ impl Bucket { } } -#[derive(Debug, enum_map::Enum, Copy, Clone, AsRefStr)] +#[derive(Debug, enum_map::Enum, Copy, Clone, Display, AsRefStr)] pub enum ActionType { Message, Register, diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 75aa56a88..547ae20d9 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -43,12 +43,8 @@ pub struct Settings { #[default(None)] #[doku(skip)] pub opentelemetry_url: Option, - /// The number of activitypub federation workers that can be in-flight concurrently - #[default(0)] - pub worker_count: usize, - /// The number of activitypub federation retry workers that can be in-flight concurrently - #[default(0)] - pub retry_count: usize, + #[default(Default::default())] + pub federation: FederationWorkerConfig, // Prometheus configuration. #[default(None)] #[doku(example = "Some(Default::default())")] @@ -237,3 +233,14 @@ pub struct PrometheusConfig { #[doku(example = "10002")] pub port: i32, } + +#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] +#[serde(default)] +// named federation"worker"config to disambiguate from the activitypub library configuration +pub struct FederationWorkerConfig { + /// Limit to the number of concurrent outgoing federation requests per target instance. + /// Set this to a higher value than 1 (e.g. 6) only if you have a huge instance (>10 activities + /// per second) and if a receiving instance is not keeping up. + #[default(1)] + pub concurrent_sends_per_instance: i8, +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 655182e1a..c1d8d359b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -97,7 +97,7 @@ services: logging: *default-logging postgres: - image: postgres:16-alpine + image: pgautoupgrade/pgautoupgrade:16-alpine # this needs to match the database host in lemmy.hson # Tune your settings via # https://pgtune.leopard.in.ua/#/ diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 72a8145ec..2befc78c5 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -20,7 +20,7 @@ x-lemmy-default: &lemmy-default restart: always x-postgres-default: &postgres-default - image: postgres:16-alpine + image: pgautoupgrade/pgautoupgrade:16-alpine environment: - POSTGRES_USER=lemmy - POSTGRES_PASSWORD=password diff --git a/scripts/postgres_12_to_15_upgrade.sh b/scripts/postgres_12_to_15_upgrade.sh index 0850e8fca..9af9af64e 100755 --- a/scripts/postgres_12_to_15_upgrade.sh +++ b/scripts/postgres_12_to_15_upgrade.sh @@ -1,48 +1,8 @@ #!/bin/sh set -e -echo "Do not stop in the middle of this upgrade, wait until you see the message: Upgrade complete." - -echo "Stopping lemmy and all services..." -sudo docker-compose stop - -echo "Make sure postgres is started..." -sudo docker-compose up -d postgres -sleep 20s - -echo "Exporting the Database to 12_15.dump.sql ..." -sudo docker-compose exec -T postgres pg_dumpall -c -U lemmy > 12_15_dump.sql -echo "Done." - -echo "Stopping postgres..." -sudo docker-compose stop postgres -sleep 20s - -echo "Removing the old postgres folder" -sudo rm -rf volumes/postgres - -echo "Updating docker-compose to use postgres version 15." -sed -i "s/image: postgres:.*/image: postgres:15-alpine/" ./docker-compose.yml - -echo "Starting up new postgres..." -sudo docker-compose up -d postgres -sleep 20s - -echo "Importing the database...." -cat 12_15_dump.sql | sudo docker-compose exec -T postgres psql -U lemmy -echo "Done." - -POSTGRES_PASSWORD=$(grep "POSTGRES_PASSWORD" ./docker-compose.yml | cut -d"=" -f2) - -echo "Fixing a weird password issue with postgres 15" -sudo docker-compose exec -T postgres psql -U lemmy -c "alter user lemmy with password '$POSTGRES_PASSWORD'" -sudo docker-compose restart postgres - -echo "Setting correct perms for pictrs folder" -sudo chown -R 991:991 volumes/pictrs +echo "Updating docker-compose to use postgres version 16." +sudo sed -i "s/image: .*postgres:.*/image: pgautoupgrade\/pgautoupgrade:16-alpine/" ./docker-compose.yml echo "Starting up lemmy..." sudo docker-compose up -d - -echo "A copy of your old database is at 12_15.dump.sql . You can delete this file if the upgrade went smoothly." -echo "Upgrade complete." diff --git a/scripts/postgres_15_to_16_upgrade.sh b/scripts/postgres_15_to_16_upgrade.sh index d83803a4f..9af9af64e 100755 --- a/scripts/postgres_15_to_16_upgrade.sh +++ b/scripts/postgres_15_to_16_upgrade.sh @@ -1,42 +1,8 @@ #!/bin/sh set -e -echo "Do not stop in the middle of this upgrade, wait until you see the message: Upgrade complete." - -echo "Stopping lemmy and all services..." -sudo docker compose stop - -echo "Make sure postgres is started..." -sudo docker compose up -d postgres -echo "Waiting..." -sleep 20s - -echo "Exporting the Database to 15_16.dump.sql ..." -sudo docker compose exec -T postgres pg_dumpall -c -U lemmy | sudo tee 15_16_dump.sql > /dev/null -echo "Done." - -echo "Stopping postgres..." -sudo docker compose stop postgres -echo "Waiting..." -sleep 20s - -echo "Removing the old postgres folder" -sudo rm -rf volumes/postgres - -echo "Updating docker compose to use postgres version 16." -sudo sed -i "s/image: .*postgres:.*/image: docker.io\/postgres:16-alpine/" ./docker-compose.yml - -echo "Starting up new postgres..." -sudo docker compose up -d postgres -echo "Waiting..." -sleep 20s - -echo "Importing the database...." -sudo cat 15_16_dump.sql | sudo docker compose exec -T postgres psql -U lemmy -echo "Done." +echo "Updating docker-compose to use postgres version 16." +sudo sed -i "s/image: .*postgres:.*/image: pgautoupgrade\/pgautoupgrade:16-alpine/" ./docker-compose.yml echo "Starting up lemmy..." -sudo docker compose up -d - -echo "A copy of your old database is at 15_16.dump.sql . You can delete this file if the upgrade went smoothly." -echo "Upgrade complete." +sudo docker-compose up -d diff --git a/scripts/test.sh b/scripts/test.sh index 9bb6acaa8..04cc94f9d 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -14,6 +14,7 @@ source scripts/start_dev_db.sh # so to load the config we need to traverse to the repo root export LEMMY_CONFIG_LOCATION=../../config/config.hjson export RUST_BACKTRACE=1 +export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min if [ -n "$PACKAGE" ]; then diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 2a15b4ca6..7b4b34158 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -82,6 +82,7 @@ use lemmy_api::{ }, registration_applications::{ approve::approve_registration_application, + get::get_registration_application, list::list_registration_applications, unread_count::get_unread_registration_application_count, }, @@ -361,6 +362,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { "/registration_application/approve", web::put().to(approve_registration_application), ) + .route( + "/registration_application", + web::get().to(get_registration_application), + ) .route("/list_all_media", web::get().to(list_all_media)) .service( web::scope("/purge") diff --git a/src/code_migrations.rs b/src/code_migrations.rs index fd4ef66de..ae6155bb6 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -468,12 +468,11 @@ async fn initialize_local_site_2022_10_10( }; let person_inserted = Person::create(pool, &person_form).await?; - let local_user_form = LocalUserInsertForm::builder() - .person_id(person_inserted.id) - .password_encrypted(setup.admin_password.clone()) - .email(setup.admin_email.clone()) - .admin(Some(true)) - .build(); + let local_user_form = LocalUserInsertForm { + email: setup.admin_email.clone(), + admin: Some(true), + ..LocalUserInsertForm::new(person_inserted.id, setup.admin_password.clone()) + }; LocalUser::create(pool, &local_user_form, vec![]).await?; }; diff --git a/src/lib.rs b/src/lib.rs index 1a772ea5f..3ba16300e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -276,6 +276,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { process_count: args.federate_process_count, }, cfg, + SETTINGS.federation.clone(), ) }); let mut interrupt = tokio::signal::unix::signal(SignalKind::interrupt())?; diff --git a/src/main.rs b/src/main.rs index dd17d6eb8..d7244d2d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,17 @@ use clap::Parser; use lemmy_server::{init_logging, start_lemmy_server, CmdArgs}; use lemmy_utils::{error::LemmyResult, settings::SETTINGS}; +pub extern crate rustls; + #[tokio::main] pub async fn main() -> LemmyResult<()> { init_logging(&SETTINGS.opentelemetry_url)?; let args = CmdArgs::parse(); + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rustls crypto provider"); + #[cfg(not(feature = "embed-pictrs"))] start_lemmy_server(args).await?; #[cfg(feature = "embed-pictrs")] diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 2885b5f74..ae6187a53 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -559,9 +559,9 @@ mod tests { #[tokio::test] #[serial] - async fn test_nodeinfo_voyager_lemmy_ml() -> LemmyResult<()> { + async fn test_nodeinfo_lemmy_ml() -> LemmyResult<()> { let client = ClientBuilder::new(client_builder(&Settings::default()).build()?).build(); - let form = build_update_instance_form("voyager.lemmy.ml", &client) + let form = build_update_instance_form("lemmy.ml", &client) .await .ok_or(LemmyErrorType::CouldntFindObject)?; assert_eq!( diff --git a/src/session_middleware.rs b/src/session_middleware.rs index 8b3090a47..a72d84920 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -146,10 +146,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("123456".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) .await