Merge pull request #1 from LemmyNet/main

Merge Lemmy main into fork
This commit is contained in:
AtHeartEngineer
2025-02-17 17:38:48 -05:00
committed by GitHub
142 changed files with 3615 additions and 1314 deletions

View File

@@ -6,13 +6,13 @@ variables:
# as well. Otherwise release builds can fail if Lemmy or dependencies rely on new Rust
# features. In particular the ARM builder image needs to be updated manually in the repo below:
# https://github.com/raskyld/lemmy-cross-toolchains
- &rust_image "rust:1.83"
- &rust_image "rust:1.81"
- &rust_nightly_image "rustlang/rust:nightly"
- &install_pnpm "npm install -g corepack@latest && corepack enable pnpm"
- &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin"
- install_diesel_cli: &install_diesel_cli
- apt-get update && apt-get install -y postgresql-client
- cargo install diesel_cli --no-default-features --features postgres
- cargo install --locked diesel_cli --no-default-features --features postgres
- export PATH="$CARGO_HOME/bin:$PATH"
- &slow_check_paths
- event: pull_request

241
Cargo.lock generated
View File

@@ -34,7 +34,7 @@ dependencies = [
"moka",
"once_cell",
"pin-project-lite",
"rand",
"rand 0.8.5",
"regex",
"reqwest 0.12.12",
"reqwest-middleware",
@@ -121,7 +121,7 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rand",
"rand 0.8.5",
"sha1",
"smallvec",
"tokio",
@@ -333,10 +333,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -484,9 +484,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.85"
version = "0.1.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
dependencies = [
"proc-macro2",
"quote",
@@ -584,13 +584,13 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bcrypt"
version = "0.16.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b1866ecef4f2d06a0bb77880015fdf2b89e25a1c2e5addacb87e459c86dc67e"
checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f"
dependencies = [
"base64 0.22.1",
"blowfish",
"getrandom",
"getrandom 0.3.1",
"subtle",
"zeroize",
]
@@ -737,7 +737,7 @@ dependencies = [
"hound",
"image",
"lodepng",
"rand",
"rand 0.8.5",
"serde_json",
]
@@ -832,9 +832,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.27"
version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
dependencies = [
"clap_builder",
"clap_derive",
@@ -842,9 +842,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.27"
version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
dependencies = [
"anstream",
"anstyle",
@@ -854,9 +854,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.24"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -1122,9 +1122,9 @@ dependencies = [
[[package]]
name = "deadpool"
version = "0.12.1"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed"
checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f"
dependencies = [
"deadpool-runtime",
"num_cpus",
@@ -1251,9 +1251,9 @@ dependencies = [
[[package]]
name = "diesel"
version = "2.2.6"
version = "2.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12"
checksum = "04001f23ba8843dc315804fa324000376084dfb1c30794ff68dd279e6e5696d5"
dependencies = [
"bitflags 2.8.0",
"byteorder",
@@ -1545,7 +1545,7 @@ checksum = "2e1f6c3800b304a6be0012039e2a45a322a093539c45ab818d9e6895a39c90fe"
dependencies = [
"proc-macro2",
"quote",
"rand",
"rand 0.8.5",
"syn 1.0.109",
]
@@ -1791,10 +1791,22 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets 0.52.6",
]
[[package]]
name = "gimli"
version = "0.31.1"
@@ -1921,13 +1933,12 @@ dependencies = [
[[package]]
name = "html2text"
version = "0.13.6"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf7722c2ffdd62628b6e13065b6ab6cf154a236bd476c6e89af1352d745b83e"
checksum = "eacd0d94e37b02109daef505556923edda7785047f24d9634b84835a8122da7a"
dependencies = [
"html5ever 0.29.0",
"markup5ever 0.14.0",
"nom",
"tendril",
"thiserror 2.0.11",
"unicode-width 0.2.0",
@@ -2136,7 +2147,7 @@ dependencies = [
"http 1.2.0",
"hyper 1.4.1",
"hyper-util",
"rustls 0.23.21",
"rustls 0.23.23",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.1",
@@ -2404,9 +2415,9 @@ dependencies = [
[[package]]
name = "infer"
version = "0.16.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847"
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
dependencies = [
"cfb",
]
@@ -2506,11 +2517,11 @@ dependencies = [
[[package]]
name = "jsonwebtoken"
version = "9.3.0"
version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64 0.21.7",
"base64 0.22.1",
"js-sys",
"pem",
"ring",
@@ -2591,7 +2602,6 @@ dependencies = [
"regex",
"reqwest 0.12.12",
"reqwest-middleware",
"rosetta-i18n",
"serde",
"serde_with",
"serial_test",
@@ -2702,7 +2712,7 @@ dependencies = [
"lemmy_utils",
"pretty_assertions",
"regex",
"rustls 0.23.21",
"rustls 0.23.23",
"serde",
"serde_json",
"serde_with",
@@ -2824,7 +2834,7 @@ dependencies = [
"mimalloc",
"reqwest-middleware",
"reqwest-tracing",
"rustls 0.23.21",
"rustls 0.23.23",
"serde_json",
"tokio",
"tracing",
@@ -2875,9 +2885,9 @@ dependencies = [
[[package]]
name = "lettre"
version = "0.11.11"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5"
checksum = "e882e1489810a45919477602194312b1a7df0e5acc30a6188be7b520268f63f8"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -2893,7 +2903,7 @@ dependencies = [
"nom",
"percent-encoding",
"quoted_printable",
"rustls 0.23.21",
"rustls 0.23.23",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"socket2",
@@ -3283,7 +3293,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -3395,7 +3405,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand",
"rand 0.8.5",
"smallvec",
"zeroize",
]
@@ -3572,7 +3582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand",
"rand 0.8.5",
]
[[package]]
@@ -3582,7 +3592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared 0.11.3",
"rand",
"rand 0.8.5",
]
[[package]]
@@ -3696,9 +3706,9 @@ checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
[[package]]
name = "postgres-protocol"
version = "0.6.7"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23"
checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54"
dependencies = [
"base64 0.22.1",
"byteorder",
@@ -3707,16 +3717,16 @@ dependencies = [
"hmac",
"md-5",
"memchr",
"rand",
"rand 0.9.0",
"sha2",
"stringprep",
]
[[package]]
name = "postgres-types"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f"
checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48"
dependencies = [
"bytes",
"fallible-iterator",
@@ -3735,7 +3745,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -3893,7 +3903,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.0.0",
"rustls 0.23.21",
"rustls 0.23.23",
"socket2",
"thiserror 1.0.69",
"tokio",
@@ -3907,10 +3917,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
dependencies = [
"bytes",
"rand",
"rand 0.8.5",
"ring",
"rustc-hash 2.0.0",
"rustls 0.23.21",
"rustls 0.23.23",
"slab",
"thiserror 1.0.69",
"tinyvec",
@@ -3952,8 +3962,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.0",
"zerocopy 0.8.17",
]
[[package]]
@@ -3963,7 +3984,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.0",
]
[[package]]
@@ -3972,7 +4003,17 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.17",
]
[[package]]
@@ -4112,7 +4153,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.21",
"rustls 0.23.23",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"serde",
@@ -4156,7 +4197,7 @@ checksum = "73e6153390585f6961341b50e5a1931d6be6dee4292283635903c26ef9d980d2"
dependencies = [
"anyhow",
"async-trait",
"getrandom",
"getrandom 0.2.15",
"http 1.2.0",
"matchit",
"reqwest 0.12.12",
@@ -4181,7 +4222,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"getrandom 0.2.15",
"libc",
"spin",
"untrusted",
@@ -4221,7 +4262,7 @@ dependencies = [
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
@@ -4294,9 +4335,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.21"
version = "0.23.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
dependencies = [
"aws-lc-rs",
"log",
@@ -4461,9 +4502,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.137"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"indexmap 2.7.0",
"itoa",
@@ -4601,7 +4642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@@ -4778,18 +4819,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -4929,9 +4970,9 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "test-context"
version = "0.3.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6676ab8513edfd2601a108621103fdb45cac9098305ca25ec93f7023b06b05d9"
checksum = "cb69cce03e432993e2dc1f93f7899b952300fcb6dc44191a1b830b60b8c3c8aa"
dependencies = [
"futures",
"test-context-macros",
@@ -4939,9 +4980,9 @@ dependencies = [
[[package]]
name = "test-context-macros"
version = "0.3.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ea17a2dc368aeca6f554343ced1b1e31f76d63683fa8016e5844bd7a5144a1"
checksum = "97e0639209021e54dbe19cafabfc0b5574b078c37358945e6d473eabe39bb974"
dependencies = [
"proc-macro2",
"quote",
@@ -5112,9 +5153,9 @@ dependencies = [
[[package]]
name = "tokio-postgres"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb"
checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0"
dependencies = [
"async-trait",
"byteorder",
@@ -5129,7 +5170,7 @@ dependencies = [
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"rand",
"rand 0.9.0",
"socket2",
"tokio",
"tokio-util",
@@ -5144,7 +5185,7 @@ checksum = "27d684bad428a0f2481f42241f821db42c54e2dc81d8c00db8536c506b0a0144"
dependencies = [
"const-oid",
"ring",
"rustls 0.23.21",
"rustls 0.23.23",
"tokio",
"tokio-postgres",
"tokio-rustls 0.26.1",
@@ -5167,7 +5208,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls 0.23.21",
"rustls 0.23.23",
"tokio",
]
@@ -5227,7 +5268,7 @@ dependencies = [
"base32",
"constant_time_eq",
"hmac",
"rand",
"rand 0.8.5",
"sha1",
"sha2",
"url",
@@ -5526,11 +5567,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.12.1"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
dependencies = [
"getrandom",
"getrandom 0.3.1",
"serde",
]
@@ -5577,6 +5618,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasite"
version = "0.1.0"
@@ -6024,6 +6074,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "write16"
version = "1.0.0"
@@ -6122,7 +6181,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713"
dependencies = [
"zerocopy-derive 0.8.17",
]
[[package]]
@@ -6136,6 +6204,17 @@ dependencies = [
"syn 2.0.96",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "zerofrom"
version = "0.1.5"

View File

@@ -92,7 +92,7 @@ lemmy_federate = { version = "=1.0.0-alpha.0", path = "./crates/federate" }
activitypub_federation = { version = "0.6.2", default-features = false, features = [
"actix-web",
] }
diesel = "2.2.6"
diesel = "2.2.7"
diesel_migrations = "2.2.0"
diesel-async = "0.5.2"
serde = { version = "1.0.217", features = ["derive"] }
@@ -119,15 +119,15 @@ reqwest-middleware = "0.3.3"
reqwest-tracing = "0.5.5"
clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.16.0"
bcrypt = "0.17.0"
chrono = { version = "0.4.39", features = [
"now",
"serde",
], default-features = false }
serde_json = { version = "1.0.137", features = ["preserve_order"] }
serde_json = { version = "1.0.138", features = ["preserve_order"] }
base64 = "0.22.1"
uuid = { version = "1.12.1", features = ["serde"] }
async-trait = "0.1.85"
uuid = { version = "1.13.1", features = ["serde"] }
async-trait = "0.1.86"
captcha = "0.0.9"
anyhow = { version = "1.0.95", features = ["backtrace"] }
diesel_ltree = "0.4.0"
@@ -137,7 +137,7 @@ regex = "1.11.1"
diesel-derive-newtype = "2.1.2"
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
enum-map = { version = "2.7" }
strum = { version = "0.26.3", features = ["derive"] }
strum = { version = "0.27.0", features = ["derive"] }
itertools = "0.14.0"
futures = "0.3.31"
http = "1.2"
@@ -147,14 +147,14 @@ ts-rs = { version = "10.1.0", features = [
"no-serde-warnings",
"url-impl",
] }
rustls = { version = "0.23.21", features = ["ring"] }
rustls = { version = "0.23.23", features = ["ring"] }
futures-util = "0.3.31"
tokio-postgres = "0.7.12"
tokio-postgres = "0.7.13"
tokio-postgres-rustls = "0.13.0"
urlencoding = "2.1.3"
moka = { version = "0.12.10", features = ["future"] }
i-love-jesus = { version = "0.1.0" }
clap = { version = "4.5.27", features = ["derive", "env"] }
clap = { version = "4.5.29", features = ["derive", "env"] }
pretty_assertions = "1.4.1"
derive-new = "0.7.0"
tuplex = "0.1.2"

View File

@@ -6,7 +6,7 @@
"repository": "https://github.com/LemmyNet/lemmy",
"author": "Dessalines",
"license": "AGPL-3.0",
"packageManager": "pnpm@9.15.0",
"packageManager": "pnpm@10.2.1+sha512.398035c7bd696d0ba0b10a688ed558285329d27ea994804a52bad9167d8e3a72bcb993f9699585d3ca25779ac64949ef422757a6c31102c12ab932e5cbe5cc92",
"scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'",
"fix": "prettier --write src && eslint --fix src",
@@ -22,16 +22,18 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^22.10.7",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0",
"eslint": "^9.18.0",
"@types/joi": "^17.2.3",
"@types/node": "^22.13.1",
"@typescript-eslint/eslint-plugin": "^8.24.0",
"@typescript-eslint/parser": "^8.24.0",
"eslint": "^9.20.0",
"eslint-plugin-prettier": "^5.2.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.20.0-search-combined.1",
"prettier": "^3.4.2",
"lemmy-js-client": "0.20.0-show-mod-reports.2",
"prettier": "^3.5.0",
"ts-jest": "^29.1.0",
"tsoa": "^6.6.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.21.0"
"typescript-eslint": "^8.24.0"
}
}

1564
api_tests/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,20 +14,22 @@ export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LE
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
PICTRS_PATH="api_tests/pict-rs"
PICTRS_EXPECTED_HASH="8feb52c0dee1dd0b41caa0e92afd9d5e597fbb4d7174d7bba22a1ba72fa01dbc pict-rs"
PICTRS_EXPECTED_HASH="7f7ac2a45ef9b13403ee139b7512135be6b060ff2f6460e0c800e18e1b49d2fd api_tests/pict-rs"
# Pictrs setup. Download file with hash check and up to 3 retries.
if [ ! -f "$PICTRS_PATH" ]; then
retry=true
count=0
while $retry && [ "$count" -lt 3 ]
while [ ! -f "$PICTRS_PATH" ] && [ "$count" -lt 3 ]
do
# This one sometimes goes down
# curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o "$PICTRS_PATH"
curl "https://codeberg.org/asonix/pict-rs/releases/download/v0.5.5/pict-rs-linux-amd64" -o "$PICTRS_PATH"
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.17-pre.9/pict-rs-linux-amd64" -o "$PICTRS_PATH"
# curl "https://codeberg.org/asonix/pict-rs/releases/download/v0.5.5/pict-rs-linux-amd64" -o "$PICTRS_PATH"
PICTRS_HASH=$(sha256sum "$PICTRS_PATH")
[[ "$PICTRS_HASH" != "$PICTRS_EXPECTED_HASH" ]] && retry=true || retry=false
let count=count+1
if [[ "$PICTRS_HASH" != "$PICTRS_EXPECTED_HASH" ]]; then
echo "Pictrs binary hash mismatch, was $PICTRS_HASH but expected $PICTRS_EXPECTED_HASH"
rm "$PICTRS_PATH"
let count=count+1
fi
done
chmod +x "$PICTRS_PATH"
fi

View File

@@ -69,7 +69,7 @@ function assertCommentFederation(
expect(commentOne?.comment.ap_id).toBe(commentTwo?.comment.ap_id);
expect(commentOne?.comment.content).toBe(commentTwo?.comment.content);
expect(commentOne?.creator.name).toBe(commentTwo?.creator.name);
expect(commentOne?.community.actor_id).toBe(commentTwo?.community.actor_id);
expect(commentOne?.community.ap_id).toBe(commentTwo?.community.ap_id);
expect(commentOne?.comment.published).toBe(commentTwo?.comment.published);
expect(commentOne?.comment.updated).toBe(commentOne?.comment.updated);
expect(commentOne?.comment.deleted).toBe(commentOne?.comment.deleted);
@@ -581,7 +581,7 @@ test("A and G subscribe to B (center) A posts, G mentions B, it gets announced t
// follow community from beta so that it accepts the mention
let betaCommunity = await resolveCommunity(
beta,
alphaCommunity.community.actor_id,
alphaCommunity.community.ap_id,
);
await followCommunity(beta, true, betaCommunity.community!.community.id);

View File

@@ -44,9 +44,7 @@ function assertCommunityFederation(
communityOne?: CommunityView,
communityTwo?: CommunityView,
) {
expect(communityOne?.community.actor_id).toBe(
communityTwo?.community.actor_id,
);
expect(communityOne?.community.ap_id).toBe(communityTwo?.community.ap_id);
expect(communityOne?.community.name).toBe(communityTwo?.community.name);
expect(communityOne?.community.title).toBe(communityTwo?.community.title);
expect(communityOne?.community.description).toBe(
@@ -198,7 +196,7 @@ test("Admin actions in remote community are not federated to origin", async () =
// gamma follows community and posts in it
let gammaCommunity = (
await resolveCommunity(gamma, communityRes.community.actor_id)
await resolveCommunity(gamma, communityRes.community.ap_id)
).community;
if (!gammaCommunity) {
throw "Missing gamma community";
@@ -206,7 +204,7 @@ test("Admin actions in remote community are not federated to origin", async () =
await followCommunity(gamma, true, gammaCommunity.community.id);
gammaCommunity = (
await waitUntil(
() => resolveCommunity(gamma, communityRes.community.actor_id),
() => resolveCommunity(gamma, communityRes.community.ap_id),
g => g.community?.subscribed === "Subscribed",
)
).community;
@@ -221,7 +219,7 @@ test("Admin actions in remote community are not federated to origin", async () =
// admin of beta decides to ban gamma from community
let betaCommunity = (
await resolveCommunity(beta, communityRes.community.actor_id)
await resolveCommunity(beta, communityRes.community.ap_id)
).community;
if (!betaCommunity) {
throw "Missing beta community";
@@ -230,7 +228,7 @@ test("Admin actions in remote community are not federated to origin", async () =
if (!bannedUserInfo1) {
throw "Missing banned user 1";
}
let bannedUserInfo2 = (await resolvePerson(beta, bannedUserInfo1.actor_id))
let bannedUserInfo2 = (await resolvePerson(beta, bannedUserInfo1.ap_id))
.person;
if (!bannedUserInfo2) {
throw "Missing banned user 2";
@@ -383,7 +381,7 @@ test("User blocks instance, communities are hidden", async () => {
test.skip("Community follower count is federated", async () => {
// Follow the beta community from alpha
let community = await createCommunity(beta);
let communityActorId = community.community_view.community.actor_id;
let communityActorId = community.community_view.community.ap_id;
let resolved = await resolveCommunity(alpha, communityActorId);
if (!resolved.community) {
throw "Missing beta community";
@@ -441,7 +439,7 @@ test("Dont receive community activities after unsubscribe", async () => {
expect(communityRes.community_view.counts.subscribers).toBe(1);
let betaCommunity = (
await resolveCommunity(beta, communityRes.community_view.community.actor_id)
await resolveCommunity(beta, communityRes.community_view.community.ap_id)
).community;
assertCommunityFederation(betaCommunity, communityRes.community_view);
@@ -503,13 +501,12 @@ test("Fetch community, includes posts", async () => {
expect(postRes.post_view.post).toBeDefined();
let resolvedCommunity = await waitUntil(
() =>
resolveCommunity(beta, communityRes.community_view.community.actor_id),
() => resolveCommunity(beta, communityRes.community_view.community.ap_id),
c => c.community?.community.id != undefined,
);
let betaCommunity = resolvedCommunity.community;
expect(betaCommunity?.community.actor_id).toBe(
communityRes.community_view.community.actor_id,
expect(betaCommunity?.community.ap_id).toBe(
communityRes.community_view.community.ap_id,
);
await longDelay();
@@ -530,7 +527,7 @@ test("Content in local-only community doesn't federate", async () => {
// cant resolve the community from another instance
await expect(
resolveCommunity(beta, communityRes.actor_id),
resolveCommunity(beta, communityRes.ap_id),
).rejects.toStrictEqual(Error("not_found"));
// create a post, also cant resolve it
@@ -545,7 +542,7 @@ test("Remote mods can edit communities", async () => {
let betaCommunity = await resolveCommunity(
beta,
communityRes.community_view.community.actor_id,
communityRes.community_view.community.ap_id,
);
if (!betaCommunity.community) {
throw "Missing beta community";
@@ -584,7 +581,7 @@ test("Community name with non-ascii chars", async () => {
let betaCommunity1 = await resolveCommunity(
beta,
communityRes.community_view.community.actor_id,
communityRes.community_view.community.ap_id,
);
expect(betaCommunity1.community!.community.name).toBe(name);

View File

@@ -75,7 +75,7 @@ test("Upload image and delete it", async () => {
expect(listAllMediaRes.images.length).toBe(previousThumbnails);
// Make sure the uploader is correct
expect(listMediaRes.images[0].person.actor_id).toBe(
expect(listMediaRes.images[0].person.ap_id).toBe(
`http://lemmy-alpha:8541/u/lemmy_alpha`,
);
@@ -268,7 +268,7 @@ test("No image proxying if setting is disabled", async () => {
let community = await createCommunity(alpha);
let betaCommunity = await resolveCommunity(
beta,
community.community_view.community.actor_id,
community.community_view.community.ap_id,
);
await followCommunity(beta, true, betaCommunity.community!.community.id);

View File

@@ -39,14 +39,12 @@ import {
listReports,
getMyUser,
listInbox,
allowInstance,
} from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView";
import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams";
import {
AddModToCommunity,
EditSite,
LemmyHttp,
PersonPostMentionView,
PostReport,
PostReportView,
@@ -98,7 +96,7 @@ async function assertPostFederation(
expect(postOne?.post.embed_description).toBe(postTwo?.post.embed_description);
expect(postOne?.post.embed_video_url).toBe(postTwo?.post.embed_video_url);
expect(postOne?.post.published).toBe(postTwo?.post.published);
expect(postOne?.community.actor_id).toBe(postTwo?.community.actor_id);
expect(postOne?.community.ap_id).toBe(postTwo?.community.ap_id);
expect(postOne?.post.locked).toBe(postTwo?.post.locked);
expect(postOne?.post.removed).toBe(postTwo?.post.removed);
expect(postOne?.post.deleted).toBe(postTwo?.post.deleted);
@@ -262,7 +260,7 @@ test("Collection of featured posts gets federated", async () => {
// fetch the community, ensure that post is also fetched and marked as featured
let betaCommunity = await resolveCommunity(
beta,
community.community_view.community.actor_id,
community.community_view.community.ap_id,
);
expect(betaCommunity).toBeDefined();
@@ -368,7 +366,7 @@ test("Remove a post from admin and community on different instance", async () =>
}
let gammaCommunity = (
await resolveCommunity(gamma, betaCommunity.community.actor_id)
await resolveCommunity(gamma, betaCommunity.community.ap_id)
).community?.community;
if (!gammaCommunity) {
throw "Missing gamma community";
@@ -407,7 +405,7 @@ test("Remove a post from admin and community on same instance", async () => {
await followBeta(alpha);
let gammaCommunity = await resolveCommunity(
gamma,
betaCommunity.community.actor_id,
betaCommunity.community.ap_id,
);
let postRes = await createPost(gamma, gammaCommunity.community!.community.id);
expect(postRes.post_view.post).toBeDefined();
@@ -469,7 +467,7 @@ test("Enforce site ban federation for local user", async () => {
// create a test user
let alphaUserHttp = await registerUser(alpha, alphaUrl);
let alphaUserPerson = (await getMyUser(alphaUserHttp)).local_user_view.person;
let alphaUserActorId = alphaUserPerson?.actor_id;
let alphaUserActorId = alphaUserPerson?.ap_id;
if (!alphaUserActorId) {
throw "Missing alpha user actor id";
}
@@ -549,7 +547,7 @@ test("Enforce site ban federation for federated user", async () => {
// create a test user
let alphaUserHttp = await registerUser(alpha, alphaUrl);
let alphaUserPerson = (await getMyUser(alphaUserHttp)).local_user_view.person;
let alphaUserActorId = alphaUserPerson?.actor_id;
let alphaUserActorId = alphaUserPerson?.ap_id;
if (!alphaUserActorId) {
throw "Missing alpha user actor id";
}
@@ -733,11 +731,12 @@ test("Report a post", async () => {
expect(betaReport.reason).toBe(gammaReport.reason);
await unfollowRemotes(alpha);
// Report was federated to poster's instance
// Report was federated to poster's instance. Alpha is not a community mod and doesnt see
// the report by default, so we need to pass show_mod_reports = true.
let alphaReport = (
(await waitUntil(
() =>
listReports(alpha).then(p =>
listReports(alpha, true).then(p =>
p.reports.find(r => {
return checkPostReportName(r, gammaReport);
}),

View File

@@ -47,7 +47,7 @@ test("Follow a private community", async () => {
// follow as new user
const user = await registerUser(beta, betaUrl);
const betaCommunity = (
await resolveCommunity(user, community.community_view.community.actor_id)
await resolveCommunity(user, community.community_view.community.ap_id)
).community;
expect(betaCommunity).toBeDefined();
expect(betaCommunity?.community.visibility).toBe("Private");
@@ -134,7 +134,7 @@ test("Only followers can view and interact with private community content", asyn
// user is not following the community and cannot view nor create posts
const user = await registerUser(beta, betaUrl);
const betaCommunity = (
await resolveCommunity(user, community.community_view.community.actor_id)
await resolveCommunity(user, community.community_view.community.ap_id)
).community!.community;
await expect(resolvePost(user, post0.post_view.post)).rejects.toStrictEqual(
Error("not_found"),
@@ -179,7 +179,7 @@ test("Reject follower", async () => {
// user is not following the community and cannot view nor create posts
const user = await registerUser(beta, betaUrl);
const betaCommunity1 = (
await resolveCommunity(user, community.community_view.community.actor_id)
await resolveCommunity(user, community.community_view.community.ap_id)
).community!.community;
// follow the community and reject
@@ -216,7 +216,7 @@ test("Follow a private community and receive activities", async () => {
// follow with users from beta and gamma
const betaCommunity = (
await resolveCommunity(beta, community.community_view.community.actor_id)
await resolveCommunity(beta, community.community_view.community.ap_id)
).community;
expect(betaCommunity).toBeDefined();
const betaCommunityId = betaCommunity!.community.id;
@@ -228,7 +228,7 @@ test("Follow a private community and receive activities", async () => {
await approveFollower(alpha, alphaCommunityId);
const gammaCommunityId = (
await resolveCommunity(gamma, community.community_view.community.actor_id)
await resolveCommunity(gamma, community.community_view.community.ap_id)
).community!.community.id;
const follow_form_gamma: FollowCommunity = {
community_id: gammaCommunityId,
@@ -281,7 +281,7 @@ test("Fetch remote content in private community", async () => {
const alphaCommunityId = community.community_view.community.id;
const betaCommunityId = (
await resolveCommunity(beta, community.community_view.community.actor_id)
await resolveCommunity(beta, community.community_view.community.ap_id)
).community!.community.id;
const follow_form_beta: FollowCommunity = {
community_id: betaCommunityId,
@@ -312,7 +312,7 @@ test("Fetch remote content in private community", async () => {
// create gamma user
const gammaCommunityId = (
await resolveCommunity(gamma, community.community_view.community.actor_id)
await resolveCommunity(gamma, community.community_view.community.ap_id)
).community!.community.id;
const follow_form: FollowCommunity = {
community_id: gammaCommunityId,

View File

@@ -83,6 +83,7 @@ import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDetailsResponse";
import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails";
import { ListingType } from "lemmy-js-client/dist/types/ListingType";
import { GetCommunityPendingFollowsCountI } from "lemmy-js-client/dist/other_types";
export const fetchFunction = fetch;
export const imageFetchLimit = 50;
@@ -800,8 +801,9 @@ export async function reportPost(
export async function listReports(
api: LemmyHttp,
show_community_rule_violations: boolean = false,
): Promise<ListReportsResponse> {
let form: ListReports = {};
let form: ListReports = { show_community_rule_violations };
return api.listReports(form);
}
@@ -882,7 +884,8 @@ export function getCommunityPendingFollowsCount(
api: LemmyHttp,
community_id: CommunityId,
): Promise<GetCommunityPendingFollowsCountResponse> {
return api.getCommunityPendingFollowsCount(community_id);
let form: GetCommunityPendingFollowsCountI = { community_id };
return api.getCommunityPendingFollowsCount(form);
}
export function approveCommunityPendingFollow(

View File

@@ -41,7 +41,7 @@ function assertUserFederation(userOne?: PersonView, userTwo?: PersonView) {
expect(userOne?.person.name).toBe(userTwo?.person.name);
expect(userOne?.person.display_name).toBe(userTwo?.person.display_name);
expect(userOne?.person.bio).toBe(userTwo?.person.bio);
expect(userOne?.person.actor_id).toBe(userTwo?.person.actor_id);
expect(userOne?.person.ap_id).toBe(userTwo?.person.ap_id);
expect(userOne?.person.avatar).toBe(userTwo?.person.avatar);
expect(userOne?.person.banner).toBe(userTwo?.person.banner);
expect(userOne?.person.published).toBe(userTwo?.person.published);

View File

@@ -14,6 +14,7 @@ pub mod login;
pub mod logout;
pub mod notifications;
pub mod report_count;
pub mod resend_verification_email;
pub mod reset_password;
pub mod save_settings;
pub mod update_totp;

View File

@@ -0,0 +1,30 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::ResendVerificationEmail,
utils::send_verification_email_if_required,
SuccessResponse,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::LemmyResult;
pub async fn resend_verification_email(
data: Json<ResendVerificationEmail>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let email = data.email.to_string();
// Fetch that email
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await?;
send_verification_email_if_required(
&context,
&site_view.local_site,
&local_user_view.local_user,
&local_user_view.person,
)
.await?;
Ok(Json(SuccessResponse::default()))
}

View File

@@ -19,7 +19,7 @@ use lemmy_db_schema::{
person::{Person, PersonUpdateForm},
},
traits::Crud,
utils::diesel_string_update,
utils::{diesel_opt_number_update, diesel_string_update},
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{
@@ -54,7 +54,9 @@ pub async fn save_user_settings(
if previous_email.deref() != email {
LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
send_verification_email(
&local_user_view,
&site_view.local_site,
&local_user_view.local_user,
&local_user_view.person,
email,
&mut context.pool(),
context.settings(),
@@ -90,6 +92,8 @@ pub async fn save_user_settings(
let person_id = local_user_view.person.id;
let default_listing_type = data.default_listing_type;
let default_post_sort_type = data.default_post_sort_type;
let default_post_time_range_seconds =
diesel_opt_number_update(data.default_post_time_range_seconds);
let default_comment_sort_type = data.default_comment_sort_type;
let person_form = PersonUpdateForm {
@@ -119,6 +123,7 @@ pub async fn save_user_settings(
blur_nsfw: data.blur_nsfw,
show_bot_accounts: data.show_bot_accounts,
default_post_sort_type,
default_post_time_range_seconds,
default_comment_sort_type,
default_listing_type,
theme: data.theme.clone(),

View File

@@ -56,6 +56,7 @@ pub async fn create_comment_report(
comment_id,
original_comment_text: comment_view.comment.content,
reason,
violates_instance_rules: data.violates_instance_rules.unwrap_or_default(),
};
let report = CommentReport::report(&mut context.pool(), &report_form)

View File

@@ -52,6 +52,7 @@ pub async fn create_post_report(
original_post_url: post_view.post.url,
original_post_body: post_view.post.body,
reason,
violates_instance_rules: data.violates_instance_rules.unwrap_or_default(),
};
let report = PostReport::report(&mut context.pool(), &report_form)

View File

@@ -14,7 +14,12 @@ pub async fn list_reports(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListReportsResponse>> {
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
let my_reports_only = data.my_reports_only;
// Only check mod or admin status when not viewing my reports
if !my_reports_only.unwrap_or_default() {
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
}
// parse pagination token
let page_after = if let Some(pa) = &data.page_cursor {
@@ -31,6 +36,8 @@ pub async fn list_reports(
unresolved_only: data.unresolved_only,
page_after,
page_back,
show_community_rule_violations: data.show_community_rule_violations,
my_reports_only,
}
.list(&mut context.pool(), &local_user_view)
.await?;

View File

@@ -18,10 +18,11 @@ pub async fn get_mod_log(
check_private_instance(&local_user_view, &local_site)?;
let type_ = data.type_;
let listing_type = data.listing_type;
let community_id = data.community_id;
let is_mod_or_admin = if let Some(local_user_view) = local_user_view {
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool())
let is_mod_or_admin = if let Some(local_user_view) = &local_user_view {
check_community_mod_of_any_or_admin_action(local_user_view, &mut context.pool())
.await
.is_ok()
} else {
@@ -37,6 +38,7 @@ pub async fn get_mod_log(
let other_person_id = data.other_person_id;
let post_id = data.post_id;
let comment_id = data.comment_id;
let local_user = local_user_view.as_ref().map(|u| &u.local_user);
// parse pagination token
let page_after = if let Some(pa) = &data.page_cursor {
@@ -48,9 +50,11 @@ pub async fn get_mod_log(
let modlog = ModlogCombinedQuery {
type_,
listing_type,
community_id,
mod_person_id,
other_person_id,
local_user,
post_id,
comment_id,
hide_modlog_names: Some(hide_modlog_names),

View File

@@ -19,7 +19,6 @@ workspace = true
[features]
full = [
"tracing",
"rosetta-i18n",
"lemmy_db_views/full",
"lemmy_utils/full",
"activitypub_federation",
@@ -51,7 +50,6 @@ chrono = { workspace = true }
tracing = { workspace = true, optional = true }
reqwest-middleware = { workspace = true, optional = true }
regex = { workspace = true }
rosetta-i18n = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
uuid = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
@@ -64,12 +62,12 @@ actix-web = { workspace = true, optional = true }
urlencoding = { workspace = true }
mime = { version = "0.3.17", optional = true }
mime_guess = "2.0.5"
infer = "0.16.0"
infer = "0.19.0"
webpage = { version = "2.0", default-features = false, optional = true, features = [
"serde",
] }
encoding_rs = { version = "0.8.35", optional = true }
jsonwebtoken = { version = "9.3.0", optional = true }
jsonwebtoken = { version = "9.3.1", optional = true }
actix-web-httpauth = { version = "0.8.2", optional = true }
webmention = { version = "0.6.0", optional = true }

View File

@@ -3,12 +3,7 @@ use crate::{
community::CommunityResponse,
context::LemmyContext,
post::PostResponse,
utils::{
check_person_instance_community_block,
get_interface_language,
is_mod_or_admin,
send_email_to_user,
},
utils::{check_person_instance_community_block, is_mod_or_admin, send_email_to_user},
};
use actix_web::web::Json;
use lemmy_db_schema::{
@@ -141,14 +136,6 @@ pub async fn send_local_notifs(
};
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
let comment_link = |comment: &Comment| {
format!(
"{}/post/{}/{}",
context.settings().get_protocol_and_hostname(),
post.id,
comment.id
)
};
// Send the local mentions
for mention in mentions
@@ -177,7 +164,10 @@ pub async fn send_local_notifs(
PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form)
.await
.ok();
(comment_link(comment), comment.content.clone())
(
comment.local_url(context.settings())?,
comment.content.clone(),
)
} else {
let person_post_mention_form = PersonPostMentionInsertForm {
recipient_id: mention_user_view.person.id,
@@ -189,17 +179,15 @@ pub async fn send_local_notifs(
PersonPostMention::create(&mut context.pool(), &person_post_mention_form)
.await
.ok();
let post_link = format!(
"{}/post/{}",
context.settings().get_protocol_and_hostname(),
post.id,
);
(post_link, post.body.clone().unwrap_or_default())
(
post.local_url(context.settings())?,
post.body.clone().unwrap_or_default(),
)
};
// Send an email to those local users that have notifications on
if do_send_email {
let lang = get_interface_language(&mention_user_view);
let lang = &mention_user_view.local_user.interface_i18n_language();
let content = markdown_to_html(&comment_content_or_post_body);
send_email_to_user(
&mention_user_view,
@@ -252,13 +240,13 @@ pub async fn send_local_notifs(
.ok();
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let lang = &parent_user_view.local_user.interface_i18n_language();
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_comment_reply_subject(&person.name),
&lang.notification_comment_reply_body(
comment_link(comment),
comment.local_url(context.settings())?,
&content,
&inbox_link,
&parent_comment.content,
@@ -305,13 +293,13 @@ pub async fn send_local_notifs(
.ok();
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let lang = &parent_user_view.local_user.interface_i18n_language();
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_post_reply_subject(&person.name),
&lang.notification_post_reply_body(
comment_link(comment),
comment.local_url(context.settings())?,
&content,
&inbox_link,
&post.name,

View File

@@ -117,6 +117,10 @@ pub struct GetComments {
#[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<CommentSortType>,
#[cfg_attr(feature = "full", ts(optional))]
/// Filter to within a given time range, in seconds.
/// IE 60 would give results for the past minute.
pub time_range_seconds: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub max_depth: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,

View File

@@ -97,6 +97,10 @@ pub struct ListCommunities {
#[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<CommunitySortType>,
#[cfg_attr(feature = "full", ts(optional))]
/// Filter to within a given time range, in seconds.
/// IE 60 would give results for the past minute.
pub time_range_seconds: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub show_nsfw: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,

View File

@@ -120,6 +120,9 @@ pub struct SaveUserSettings {
/// The default post sort, usually "active"
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_sort_type: Option<PostSortType>,
/// A default time range limit to apply to post sorts, in seconds. 0 means none.
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_time_range_seconds: Option<i32>,
/// The default comment sort, usually "hot"
#[cfg_attr(feature = "full", ts(optional))]
pub default_comment_sort_type: Option<CommentSortType>,
@@ -532,3 +535,11 @@ pub struct ListMediaResponse {
pub struct ListLoginsResponse {
pub logins: Vec<LoginToken>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Make a request to resend your verification email.
pub struct ResendVerificationEmail {
pub email: SensitiveString,
}

View File

@@ -4,13 +4,7 @@ use lemmy_db_schema::{
PostFeatureType,
PostSortType,
};
use lemmy_db_views::structs::{
CommunityModeratorView,
CommunityView,
PaginationCursor,
PostView,
VoteView,
};
use lemmy_db_views::structs::{CommunityView, PaginationCursor, PostView, VoteView};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
@@ -77,7 +71,6 @@ pub struct GetPost {
pub struct GetPostResponse {
pub post_view: PostView,
pub community_view: CommunityView,
pub moderators: Vec<CommunityModeratorView>,
/// A list of cross-posts, or other times / communities this link has been posted to.
pub cross_posts: Vec<PostView>,
}
@@ -92,6 +85,11 @@ pub struct GetPosts {
pub type_: Option<ListingType>,
#[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<PostSortType>,
#[cfg_attr(feature = "full", ts(optional))]
/// Filter to within a given time range, in seconds.
/// IE 60 would give results for the past minute.
/// Use Zero to override the local_site and local_user time_range.
pub time_range_seconds: Option<i32>,
/// DEPRECATED, use page_cursor
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,

View File

@@ -30,6 +30,12 @@ pub struct ListReports {
pub page_cursor: Option<ReportCombinedPaginationCursor>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_back: Option<bool>,
/// Only for admins: also show reports with `violates_instance_rules=false`
#[cfg_attr(feature = "full", ts(optional))]
pub show_community_rule_violations: Option<bool>,
/// If true, view all your created reports. Works for non-admins/mods also.
#[cfg_attr(feature = "full", ts(optional))]
pub my_reports_only: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View File

@@ -11,6 +11,8 @@ use ts_rs::TS;
pub struct CreateCommentReport {
pub comment_id: CommentId,
pub reason: String,
#[cfg_attr(feature = "full", ts(optional))]
pub violates_instance_rules: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View File

@@ -11,6 +11,8 @@ use ts_rs::TS;
pub struct CreatePostReport {
pub post_id: PostId,
pub reason: String,
#[cfg_attr(feature = "full", ts(optional))]
pub violates_instance_rules: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View File

@@ -10,7 +10,7 @@ use chrono::{DateTime, Utc};
use encoding_rs::{Encoding, UTF_8};
use futures::StreamExt;
use lemmy_db_schema::source::{
images::{ImageDetailsForm, LocalImage, LocalImageForm},
images::{ImageDetailsInsertForm, LocalImage, LocalImageForm},
post::{Post, PostUpdateForm},
site::Site,
};
@@ -339,17 +339,19 @@ pub struct PictrsFileDetails {
pub height: u16,
pub content_type: String,
pub created_at: DateTime<Utc>,
pub blurhash: Option<String>,
}
impl PictrsFileDetails {
/// Builds the image form. This should always use the thumbnail_url,
/// Because the post_view joins to it
pub fn build_image_details_form(&self, thumbnail_url: &Url) -> ImageDetailsForm {
ImageDetailsForm {
pub fn build_image_details_form(&self, thumbnail_url: &Url) -> ImageDetailsInsertForm {
ImageDetailsInsertForm {
link: thumbnail_url.clone().into(),
width: self.width.into(),
height: self.height.into(),
content_type: self.content_type.clone(),
blurhash: self.blurhash.clone(),
}
}
}

View File

@@ -69,6 +69,10 @@ pub struct Search {
#[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<SearchSortType>,
#[cfg_attr(feature = "full", ts(optional))]
/// Filter to within a given time range, in seconds.
/// IE 60 would give results for the past minute.
pub time_range_seconds: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub listing_type: Option<ListingType>,
#[cfg_attr(feature = "full", ts(optional))]
pub title_only: Option<bool>,
@@ -124,16 +128,26 @@ pub struct ResolveObjectResponse {
#[cfg_attr(feature = "full", ts(export))]
/// Fetches the modlog.
pub struct GetModlog {
/// Filter by the moderator.
#[cfg_attr(feature = "full", ts(optional))]
pub mod_person_id: Option<PersonId>,
/// Filter by the community.
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>,
/// Filter by the modlog action type.
#[cfg_attr(feature = "full", ts(optional))]
pub type_: Option<ModlogActionType>,
/// Filter by listing type. When not using All, it will remove the non-community modlog entries,
/// such as site bans, instance blocks, adding an admin, etc.
#[cfg_attr(feature = "full", ts(optional))]
pub listing_type: Option<ListingType>,
/// Filter by the other / modded person.
#[cfg_attr(feature = "full", ts(optional))]
pub other_person_id: Option<PersonId>,
/// Filter by post. Will include comments of that post.
#[cfg_attr(feature = "full", ts(optional))]
pub post_id: Option<PostId>,
/// Filter by comment.
#[cfg_attr(feature = "full", ts(optional))]
pub comment_id: Option<CommentId>,
#[cfg_attr(feature = "full", ts(optional))]
@@ -179,6 +193,8 @@ pub struct CreateSite {
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_sort_type: Option<PostSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_time_range_seconds: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub default_comment_sort_type: Option<CommentSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub legal_information: Option<String>,
@@ -278,6 +294,9 @@ pub struct EditSite {
/// The default post sort, usually "active"
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_sort_type: Option<PostSortType>,
/// A default time range limit to apply to post sorts, in seconds. 0 means none.
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_time_range_seconds: Option<i32>,
/// The default comment sort, usually "hot"
#[cfg_attr(feature = "full", ts(optional))]
pub default_comment_sort_type: Option<CommentSortType>,

View File

@@ -26,6 +26,7 @@ use lemmy_db_schema::{
local_site::LocalSite,
local_site_rate_limit::LocalSiteRateLimit,
local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::LocalUser,
mod_log::moderator::{
ModRemoveComment,
ModRemoveCommentForm,
@@ -37,6 +38,7 @@ use lemmy_db_schema::{
person::{Person, PersonUpdateForm},
person_block::PersonBlock,
post::{Post, PostLike},
private_message::PrivateMessage,
registration_application::RegistrationApplication,
site::Site,
},
@@ -59,7 +61,7 @@ use lemmy_db_views::{
},
};
use lemmy_utils::{
email::{send_email, translations::Lang},
email::send_email,
error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
rate_limit::{ActionType, BucketConfig},
settings::{
@@ -77,7 +79,6 @@ use lemmy_utils::{
};
use moka::future::Cache;
use regex::{escape, Regex, RegexSet};
use rosetta_i18n::{Language, LanguageId};
use std::sync::LazyLock;
use tracing::{warn, Instrument};
use url::{ParseError, Url};
@@ -445,7 +446,7 @@ pub async fn send_password_reset_email(
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language(user);
let lang = &user.local_user.interface_i18n_language();
let subject = &lang.password_reset_subject(&user.person.name);
let protocol_and_hostname = settings.get_protocol_and_hostname();
let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token);
@@ -461,13 +462,15 @@ pub async fn send_password_reset_email(
/// Send a verification email
pub async fn send_verification_email(
user: &LocalUserView,
local_site: &LocalSite,
local_user: &LocalUser,
person: &Person,
new_email: &str,
pool: &mut DbPool<'_>,
settings: &Settings,
) -> LemmyResult<()> {
let form = EmailVerificationForm {
local_user_id: user.local_user.id,
local_user_id: local_user.id,
email: new_email.to_string(),
verification_token: uuid::Uuid::new_v4().to_string(),
};
@@ -478,29 +481,45 @@ pub async fn send_verification_email(
);
EmailVerification::create(pool, &form).await?;
let lang = get_interface_language(user);
let lang = local_user.interface_i18n_language();
let subject = lang.verify_email_subject(&settings.hostname);
let body = lang.verify_email_body(&settings.hostname, &user.person.name, verify_link);
send_email(&subject, new_email, &user.person.name, &body, settings).await?;
Ok(())
// If an application is required, use a translation that includes that warning.
let body = if local_site.registration_mode == RegistrationMode::RequireApplication {
lang.verify_email_body_with_application(&settings.hostname, &person.name, verify_link)
} else {
lang.verify_email_body(&settings.hostname, &person.name, verify_link)
};
send_email(&subject, new_email, &person.name, &body, settings).await
}
pub fn get_interface_language(user: &LocalUserView) -> Lang {
lang_str_to_lang(&user.local_user.interface_language)
}
/// Returns true if email was sent.
pub async fn send_verification_email_if_required(
context: &LemmyContext,
local_site: &LocalSite,
local_user: &LocalUser,
person: &Person,
) -> LemmyResult<bool> {
let email = &local_user
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
pub fn get_interface_language_from_settings(user: &LocalUserView) -> Lang {
lang_str_to_lang(&user.local_user.interface_language)
}
#[allow(clippy::expect_used)]
fn lang_str_to_lang(lang: &str) -> Lang {
let lang_id = LanguageId::new(lang);
Lang::from_language_id(&lang_id).unwrap_or_else(|| {
let en = LanguageId::new("en");
Lang::from_language_id(&en).expect("default language")
})
if !local_user.admin && local_site.require_email_verification && !local_user.email_verified {
send_verification_email(
local_site,
local_user,
person,
email,
&mut context.pool(),
context.settings(),
)
.await?;
Ok(true)
} else {
Ok(false)
}
}
pub fn local_site_rate_limit_to_rate_limit_config(
@@ -567,8 +586,8 @@ pub async fn send_application_approved_email(
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language(user);
let subject = lang.registration_approved_subject(&user.person.actor_id);
let lang = &user.local_user.interface_i18n_language();
let subject = lang.registration_approved_subject(&user.person.ap_id);
let body = lang.registration_approved_body(&settings.hostname);
send_email(&subject, email, &user.person.name, &body, settings).await
}
@@ -593,7 +612,7 @@ pub async fn send_new_applicant_email_to_admins(
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language_from_settings(admin);
let lang = &admin.local_user.interface_i18n_language();
let subject = lang.new_application_subject(&settings.hostname, applicant_username);
let body = lang.new_application_body(applications_link);
send_email(&subject, email, &admin.person.name, &body, settings).await?;
@@ -615,7 +634,7 @@ pub async fn send_new_report_email_to_admins(
for admin in &admins {
if let Some(email) = &admin.local_user.email {
let lang = get_interface_language_from_settings(admin);
let lang = &admin.local_user.interface_i18n_language();
let subject =
lang.new_report_subject(&settings.hostname, reported_username, reporter_username);
let body = lang.new_report_body(reports_link);
@@ -633,14 +652,14 @@ pub fn check_private_instance_and_federation_enabled(local_site: &LocalSite) ->
}
}
/// Read the site for an actor_id.
/// Read the site for an ap_id.
///
/// Used for GetCommunityResponse and GetPersonDetails
pub async fn read_site_for_actor(
actor_id: DbUrl,
ap_id: DbUrl,
context: &LemmyContext,
) -> LemmyResult<Option<Site>> {
let site_id = Site::instance_actor_id_from_url(actor_id.clone().into());
let site_id = Site::instance_ap_id_from_url(ap_id.clone().into());
let site = Site::read_from_apub_id(&mut context.pool(), &site_id.into()).await?;
Ok(site)
}
@@ -807,6 +826,9 @@ pub async fn remove_or_restore_user_data(
)
.await?;
// Private messages
PrivateMessage::update_removed_for_creator(pool, banned_person_id, removed).await?;
Ok(())
}
@@ -954,33 +976,8 @@ pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) ->
Ok(())
}
pub enum EndpointType {
Community,
Person,
Post,
Comment,
PrivateMessage,
}
/// Generates an apub endpoint for a given domain, IE xyz.tld
pub fn generate_local_apub_endpoint(
endpoint_type: EndpointType,
name: &str,
domain: &str,
) -> Result<DbUrl, ParseError> {
let point = match endpoint_type {
EndpointType::Community => "c",
EndpointType::Person => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Ok(Url::parse(&format!("{domain}/{point}/{name}"))?.into())
}
pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{actor_id}/followers"))?.into())
pub fn generate_followers_url(ap_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{ap_id}/followers"))?.into())
}
pub fn generate_inbox_url() -> LemmyResult<DbUrl> {
@@ -988,12 +985,12 @@ pub fn generate_inbox_url() -> LemmyResult<DbUrl> {
Ok(Url::parse(&url)?.into())
}
pub fn generate_outbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{actor_id}/outbox"))?.into())
pub fn generate_outbox_url(ap_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{ap_id}/outbox"))?.into())
}
pub fn generate_featured_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{actor_id}/featured"))?.into())
pub fn generate_featured_url(ap_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{ap_id}/featured"))?.into())
}
pub fn generate_moderators_url(community_id: &DbUrl) -> LemmyResult<DbUrl> {

View File

@@ -8,12 +8,10 @@ use lemmy_api_common::{
utils::{
generate_followers_url,
generate_inbox_url,
generate_local_apub_endpoint,
get_url_blocklist,
is_admin,
local_site_to_slur_regex,
process_markdown_opt,
EndpointType,
},
};
use lemmy_db_schema::{
@@ -82,13 +80,8 @@ pub async fn create_community(
check_community_visibility_allowed(data.visibility, &local_user_view)?;
// Double check for duplicate community actor_ids
let community_actor_id = generate_local_apub_endpoint(
EndpointType::Community,
&data.name,
&context.settings().get_protocol_and_hostname(),
)?;
let community_dupe =
Community::read_from_apub_id(&mut context.pool(), &community_actor_id).await?;
let community_ap_id = Community::local_url(&data.name, context.settings())?;
let community_dupe = Community::read_from_apub_id(&mut context.pool(), &community_ap_id).await?;
if community_dupe.is_some() {
Err(LemmyErrorType::CommunityAlreadyExists)?
}
@@ -100,9 +93,9 @@ pub async fn create_community(
sidebar,
description,
nsfw: data.nsfw,
actor_id: Some(community_actor_id.clone()),
ap_id: Some(community_ap_id.clone()),
private_key: Some(keypair.private_key),
followers_url: Some(generate_followers_url(&community_actor_id)?),
followers_url: Some(generate_followers_url(&community_ap_id)?),
inbox_url: Some(generate_inbox_url()?),
posting_restricted_to_mods: data.posting_restricted_to_mods,
visibility: data.visibility,

View File

@@ -24,15 +24,18 @@ pub async fn list_communities(
check_private_instance(&local_user_view, &local_site.local_site)?;
let sort = data.sort;
let time_range_seconds = data.time_range_seconds;
let listing_type = data.type_;
let show_nsfw = data.show_nsfw.unwrap_or_default();
let page = data.page;
let limit = data.limit;
let local_user = local_user_view.map(|l| l.local_user);
let communities = CommunityQuery {
listing_type,
show_nsfw,
sort,
time_range_seconds,
local_user: local_user.as_ref(),
page,
limit,

View File

@@ -13,7 +13,7 @@ use lemmy_db_schema::{
};
use lemmy_db_views::{
post::post_view::PostQuery,
structs::{CommunityModeratorView, CommunityView, LocalUserView, PostView, SiteView},
structs::{CommunityView, LocalUserView, PostView, SiteView},
};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
@@ -84,8 +84,6 @@ pub async fn get_post(
)
.await?;
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
// Fetch the cross_posts
let cross_posts = if let Some(url) = &post_view.post.url {
let mut x_posts = PostQuery {
@@ -108,7 +106,6 @@ pub async fn get_post(
Ok(Json(GetPostResponse {
post_view,
community_view,
moderators,
cross_posts,
}))
}

View File

@@ -6,7 +6,6 @@ use lemmy_api_common::{
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_private_messages_enabled,
get_interface_language,
get_url_blocklist,
local_site_to_slur_regex,
process_markdown,
@@ -72,7 +71,7 @@ pub async fn create_private_message(
if view.recipient.local {
let recipient_id = data.recipient_id;
let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?;
let lang = get_interface_language(&local_recipient);
let lang = &local_recipient.local_user.interface_i18n_language();
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
let sender_name = &local_user_view.person.name;
let content = markdown_to_html(&content);

View File

@@ -53,7 +53,7 @@ pub async fn create_site(
validate_create_payload(&local_site, &data)?;
let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
let ap_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
let inbox_url = Some(generate_inbox_url()?);
let keypair = generate_actor_keypair()?;
@@ -65,7 +65,7 @@ pub async fn create_site(
name: Some(data.name.clone()),
sidebar: diesel_string_update(sidebar.as_deref()),
description: diesel_string_update(data.description.as_deref()),
actor_id: Some(actor_id),
ap_id: Some(ap_id),
last_refreshed_at: Some(Utc::now()),
inbox_url,
private_key: Some(Some(keypair.private_key)),

View File

@@ -24,7 +24,7 @@ use lemmy_db_schema::{
site::{Site, SiteUpdateForm},
},
traits::Crud,
utils::diesel_string_update,
utils::{diesel_opt_number_update, diesel_string_update},
RegistrationMode,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
@@ -68,6 +68,8 @@ pub async fn update_site(
.await?
.as_deref(),
);
let default_post_time_range_seconds =
diesel_opt_number_update(data.default_post_time_range_seconds);
let site_form = SiteUpdateForm {
name: data.name.clone(),
@@ -93,6 +95,7 @@ pub async fn update_site(
default_theme: data.default_theme.clone(),
default_post_listing_type: data.default_post_listing_type,
default_post_sort_type: data.default_post_sort_type,
default_post_time_range_seconds,
default_comment_sort_type: data.default_comment_sort_type,
legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins,

View File

@@ -10,17 +10,14 @@ use lemmy_api_common::{
check_registration_application,
check_user_valid,
generate_inbox_url,
generate_local_apub_endpoint,
honeypot_check,
local_site_to_slur_regex,
password_length_check,
send_new_applicant_email_to_admins,
send_verification_email,
EndpointType,
send_verification_email_if_required,
},
};
use lemmy_db_schema::{
aggregates::structs::PersonAggregates,
newtypes::{InstanceId, OAuthProviderId},
source::{
actor_language::SiteLanguage,
@@ -28,7 +25,6 @@ use lemmy_db_schema::{
language::Language,
local_site::LocalSite,
local_user::{LocalUser, LocalUserInsertForm},
local_user_vote_display_mode::LocalUserVoteDisplayMode,
oauth_account::{OAuthAccount, OAuthAccountInsertForm},
oauth_provider::OAuthProvider,
person::{Person, PersonInsertForm},
@@ -414,15 +410,11 @@ async fn create_person(
) -> Result<Person, LemmyError> {
let actor_keypair = generate_actor_keypair()?;
is_valid_actor_name(&username, local_site.actor_name_max_length as usize)?;
let actor_id = generate_local_apub_endpoint(
EndpointType::Person,
&username,
&context.settings().get_protocol_and_hostname(),
)?;
let ap_id = Person::local_url(&username, context.settings())?;
// Register the new person
let person_form = PersonInsertForm {
actor_id: Some(actor_id.clone()),
ap_id: Some(ap_id.clone()),
inbox_url: Some(generate_inbox_url()?),
private_key: Some(actor_keypair.private_key),
..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id)
@@ -482,37 +474,6 @@ async fn create_local_user(
Ok(inserted_local_user)
}
async fn send_verification_email_if_required(
context: &Data<LemmyContext>,
local_site: &LocalSite,
local_user: &LocalUser,
person: &Person,
) -> LemmyResult<bool> {
let mut sent = false;
if !local_user.admin && local_site.require_email_verification && !local_user.email_verified {
let local_user_view = LocalUserView {
local_user: local_user.clone(),
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
person: person.clone(),
counts: PersonAggregates::default(),
};
send_verification_email(
&local_user_view,
&local_user
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?,
&mut context.pool(),
context.settings(),
)
.await?;
sent = true;
}
Ok(sent)
}
fn validate_registration_answer(
require_registration_application: bool,
answer: &Option<String>,

View File

@@ -41,7 +41,7 @@ reqwest = { workspace = true }
moka.workspace = true
serde_with.workspace = true
html2md = "0.2.15"
html2text = "0.13.6"
html2text = "0.14.0"
stringreader = "0.1.1"
enum_delegate = "0.2.0"
semver = "1.0.25"

View File

@@ -110,8 +110,8 @@ impl Object for SiteOrCommunity {
impl SiteOrCommunity {
fn id(&self) -> ObjectId<SiteOrCommunity> {
match self {
SiteOrCommunity::Site(s) => ObjectId::from(s.actor_id.clone()),
SiteOrCommunity::Community(c) => ObjectId::from(c.actor_id.clone()),
SiteOrCommunity::Site(s) => ObjectId::from(s.ap_id.clone()),
SiteOrCommunity::Community(c) => ObjectId::from(c.ap_id.clone()),
}
}
}
@@ -121,7 +121,7 @@ async fn generate_cc(target: &SiteOrCommunity, pool: &mut DbPool<'_>) -> LemmyRe
SiteOrCommunity::Site(_) => Site::read_remote_sites(pool)
.await?
.into_iter()
.map(|s| s.actor_id.into())
.map(|s| s.ap_id.into())
.collect(),
SiteOrCommunity::Community(c) => vec![c.id()],
})

View File

@@ -62,13 +62,13 @@ impl ActivityHandler for RawAnnouncableActivities {
// verify and receive activity
activity.verify(context).await?;
let actor_id = activity.actor().clone().into();
let ap_id = activity.actor().clone().into();
activity.receive(context).await?;
// if community is local, send activity to followers
if let Some(community) = community {
if community.local {
verify_person_in_community(&actor_id, &community, context).await?;
verify_person_in_community(&ap_id, &community, context).await?;
AnnounceActivity::send(self, &community, context).await?;
}
}

View File

@@ -55,7 +55,7 @@ impl CollectionAdd {
actor: actor.id().into(),
to: generate_to(community)?,
object: added_mod.id(),
target: generate_moderators_url(&community.actor_id)?.into(),
target: generate_moderators_url(&community.ap_id)?.into(),
cc: vec![community.id()],
kind: AddType::Add,
id: id.clone(),
@@ -80,7 +80,7 @@ impl CollectionAdd {
actor: actor.id().into(),
to: generate_to(community)?,
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
target: generate_featured_url(&community.ap_id)?.into(),
cc: vec![community.id()],
kind: AddType::Add,
id: id.clone(),

View File

@@ -50,7 +50,7 @@ impl CollectionRemove {
actor: actor.id().into(),
to: generate_to(community)?,
object: removed_mod.id(),
target: generate_moderators_url(&community.actor_id)?.into(),
target: generate_moderators_url(&community.ap_id)?.into(),
id: id.clone(),
cc: vec![community.id()],
kind: RemoveType::Remove,
@@ -75,7 +75,7 @@ impl CollectionRemove {
actor: actor.id().into(),
to: generate_to(community)?,
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
target: generate_featured_url(&community.ap_id)?.into(),
cc: vec![community.id()],
kind: RemoveType::Remove,
id: id.clone(),

View File

@@ -135,9 +135,9 @@ pub(crate) async fn send_lock_post(
LockType::Lock,
&context.settings().get_protocol_and_hostname(),
)?;
let community_id = community.actor_id.inner().clone();
let community_id = community.ap_id.inner().clone();
let lock = LockPage {
actor: actor.actor_id.clone().into(),
actor: actor.ap_id.clone().into(),
to: generate_to(&community)?,
object: ObjectId::from(post.ap_id),
cc: vec![community_id.clone()],

View File

@@ -105,6 +105,7 @@ impl ActivityHandler for Report {
original_post_url: post.url.clone(),
reason,
original_post_body: post.body.clone(),
violates_instance_rules: false,
};
PostReport::report(&mut context.pool(), &report_form).await?;
}
@@ -116,6 +117,7 @@ impl ActivityHandler for Report {
comment_id: comment.id,
original_comment_text: comment.content.clone(),
reason,
violates_instance_rules: false,
};
CommentReport::report(&mut context.pool(), &report_form).await?;
}

View File

@@ -80,7 +80,7 @@ impl ActivityHandler for UpdateCommunity {
verify_visibility(&self.to, &self.cc, &community)?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_mod_action(&self.actor, &community, context).await?;
ApubCommunity::verify(&self.object, &community.actor_id.clone().into(), context).await?;
ApubCommunity::verify(&self.object, &community.ap_id.clone().into(), context).await?;
Ok(())
}
@@ -98,7 +98,7 @@ impl ActivityHandler for UpdateCommunity {
published: self.object.published,
updated: Some(self.object.updated),
nsfw: Some(self.object.sensitive.unwrap_or(false)),
actor_id: Some(self.object.id.into()),
ap_id: Some(self.object.id.into()),
public_key: Some(self.object.public_key.public_key_pem),
last_refreshed_at: Some(Utc::now()),
icon: Some(self.object.icon.map(|i| i.url.into())),

View File

@@ -91,9 +91,9 @@ impl Delete {
DeleteType::Delete,
&context.settings().get_protocol_and_hostname(),
)?;
let cc: Option<Url> = community.map(|c| c.actor_id.clone().into());
let cc: Option<Url> = community.map(|c| c.ap_id.clone().into());
Ok(Delete {
actor: actor.actor_id.clone().into(),
actor: actor.ap_id.clone().into(),
to,
object: IdOrNestedObject::Id(object.id()),
cc: cc.into_iter().collect(),

View File

@@ -180,7 +180,7 @@ pub(in crate::activities) async fn verify_delete_activity(
DeletableObjects::Person(person) => {
verify_is_public(&activity.to, &[])?;
verify_person(&activity.actor, context).await?;
verify_urls_match(person.actor_id.inner(), activity.object.id())?;
verify_urls_match(person.ap_id.inner(), activity.object.id())?;
}
DeletableObjects::Post(p) => {
let community = activity.community(context).await?;

View File

@@ -77,9 +77,9 @@ impl UndoDelete {
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?;
let cc: Option<Url> = community.map(|c| c.actor_id.clone().into());
let cc: Option<Url> = community.map(|c| c.ap_id.clone().into());
Ok(UndoDelete {
actor: actor.actor_id.clone().into(),
actor: actor.ap_id.clone().into(),
to,
object,
cc: cc.into_iter().collect(),

View File

@@ -21,12 +21,13 @@ use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
community::{CommunityFollower, CommunityFollowerForm, CommunityFollowerState},
instance::Instance,
person::{PersonFollower, PersonFollowerForm},
},
traits::Followable,
CommunityVisibility,
};
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorType, LemmyResult};
use url::Url;
impl Follow {
@@ -102,6 +103,13 @@ impl ActivityHandler for Follow {
AcceptFollow::send(self, context).await?;
}
UserOrCommunity::Community(c) => {
if c.visibility == CommunityVisibility::Private {
let instance = Instance::read(&mut context.pool(), actor.instance_id).await?;
if [Some("kbin"), Some("mbin")].contains(&instance.software.as_deref()) {
// TODO: change this to a minimum version check once private communities are supported
return Err(FederationError::PlatformLackingPrivateCommunitySupport.into());
}
}
let state = Some(match c.visibility {
CommunityVisibility::Public => CommunityFollowerState::Accepted,
CommunityVisibility::Private => CommunityFollowerState::ApprovalRequired,

View File

@@ -47,9 +47,9 @@ pub async fn send_accept_or_reject_follow(
let person = Person::read(&mut context.pool(), person_id).await?;
let follow = Follow {
actor: person.actor_id.into(),
to: Some([community.actor_id.clone().into()]),
object: community.actor_id.into(),
actor: person.ap_id.into(),
to: Some([community.ap_id.clone().into()]),
object: community.ap_id.into(),
kind: FollowType::Follow,
id: generate_activity_id(
FollowType::Follow,

View File

@@ -82,7 +82,7 @@ pub(crate) async fn verify_person_in_community(
let person = person_id.dereference(context).await?;
if person.banned {
Err(FederationError::PersonIsBannedFromSite(
person.actor_id.to_string(),
person.ap_id.to_string(),
))?
}
let person_id = person.id;
@@ -103,7 +103,7 @@ pub(crate) async fn verify_mod_action(
// mod action comes from the same instance as the community, so it was presumably done
// by an instance admin.
// TODO: federate instance admin status and check it here
if mod_id.inner().domain() == community.actor_id.domain() {
if mod_id.inner().domain() == community.ap_id.domain() {
return Ok(());
}
@@ -134,13 +134,13 @@ pub(crate) fn verify_visibility(to: &[Url], cc: &[Url], community: &Community) -
/// Marks object as public only if the community is public
pub(crate) fn generate_to(community: &Community) -> LemmyResult<Vec<Url>> {
let actor_id = community.actor_id.clone().into();
let ap_id = community.ap_id.clone().into();
if community.visibility == CommunityVisibility::Public {
Ok(vec![actor_id, public()])
Ok(vec![ap_id, public()])
} else {
Ok(vec![
actor_id.clone(),
Url::parse(&format!("{}/followers", actor_id))?,
ap_id.clone(),
Url::parse(&format!("{}/followers", ap_id))?,
])
}
}

View File

@@ -1,7 +1,7 @@
use super::comment_sort_type_with_default;
use crate::{
api::listing_type_with_default,
fetcher::resolve_actor_identifier,
fetcher::resolve_ap_identifier,
objects::community::ApubCommunity,
};
use activitypub_federation::config::Data;
@@ -32,7 +32,7 @@ async fn list_comments_common(
let community_id = if let Some(name) = &data.community_name {
Some(
resolve_actor_identifier::<ApubCommunity, Community>(name, &context, &local_user_view, true)
resolve_ap_identifier::<ApubCommunity, Community>(name, &context, &local_user_view, true)
.await?,
)
.map(|c| c.id)
@@ -45,6 +45,7 @@ async fn list_comments_common(
local_user_ref,
&site_view.local_site,
));
let time_range_seconds = data.time_range_seconds;
let max_depth = data.max_depth;
let liked_only = data.liked_only;
@@ -76,6 +77,7 @@ async fn list_comments_common(
CommentQuery {
listing_type,
sort,
time_range_seconds,
max_depth,
liked_only,
disliked_only,

View File

@@ -1,6 +1,10 @@
use crate::{
api::{listing_type_with_default, post_sort_type_with_default},
fetcher::resolve_actor_identifier,
api::{
listing_type_with_default,
post_sort_type_with_default,
post_time_range_seconds_with_default,
},
fetcher::resolve_ap_identifier,
objects::community::ApubCommunity,
};
use activitypub_federation::config::Data;
@@ -25,15 +29,15 @@ pub async fn list_posts(
context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<GetPostsResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site.local_site)?;
check_private_instance(&local_user_view, &site_view.local_site)?;
let page = data.page;
let limit = data.limit;
let community_id = if let Some(name) = &data.community_name {
Some(
resolve_actor_identifier::<ApubCommunity, Community>(name, &context, &local_user_view, true)
resolve_ap_identifier::<ApubCommunity, Community>(name, &context, &local_user_view, true)
.await?,
)
.map(|c| c.id)
@@ -55,15 +59,20 @@ pub async fn list_posts(
let listing_type = Some(listing_type_with_default(
data.type_,
local_user,
&local_site.local_site,
&site_view.local_site,
community_id,
));
let sort = Some(post_sort_type_with_default(
data.sort,
local_user,
&local_site.local_site,
&site_view.local_site,
));
let time_range_seconds = post_time_range_seconds_with_default(
data.time_range_seconds,
local_user,
&site_view.local_site,
);
// parse pagination token
let page_after = if let Some(pa) = &data.page_cursor {
@@ -76,6 +85,7 @@ pub async fn list_posts(
local_user,
listing_type,
sort,
time_range_seconds,
community_id,
read_only,
liked_only,
@@ -90,7 +100,7 @@ pub async fn list_posts(
no_comments_only,
..Default::default()
}
.list(&local_site.site, &mut context.pool())
.list(&site_view.site, &mut context.pool())
.await
.with_lemmy_type(LemmyErrorType::CouldntGetPosts)?;

View File

@@ -1,4 +1,4 @@
use crate::{fetcher::resolve_actor_identifier, objects::person::ApubPerson};
use crate::{fetcher::resolve_ap_identifier, objects::person::ApubPerson};
use activitypub_federation::config::Data;
use lemmy_api_common::{context::LemmyContext, LemmyErrorType};
use lemmy_db_schema::{
@@ -54,6 +54,26 @@ fn post_sort_type_with_default(
)
}
/// Returns a default post_time_range.
/// Order is the given, then local user default, then site default.
/// If zero is given, then the output is None.
fn post_time_range_seconds_with_default(
secs: Option<i32>,
local_user: Option<&LocalUser>,
local_site: &LocalSite,
) -> Option<i32> {
let out = secs
.or(local_user.and_then(|u| u.default_post_time_range_seconds))
.or(local_site.default_post_time_range_seconds);
// A zero is an override to None
if out.is_some_and(|o| o == 0) {
None
} else {
out
}
}
/// Returns a default instance-level comment sort type, if none is given by the user.
/// Order is type, local user default, then site default.
fn comment_sort_type_with_default(
@@ -83,7 +103,7 @@ async fn resolve_person_id_from_id_or_username(
Some(id) => *id,
None => {
if let Some(username) = username {
resolve_actor_identifier::<ApubPerson, Person>(username, context, local_user_view, true)
resolve_ap_identifier::<ApubPerson, Person>(username, context, local_user_view, true)
.await?
.id
} else {

View File

@@ -1,4 +1,4 @@
use crate::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
use crate::{fetcher::resolve_ap_identifier, objects::community::ApubCommunity};
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use lemmy_api_common::{
@@ -33,7 +33,7 @@ pub async fn get_community(
Some(id) => id,
None => {
let name = data.name.clone().unwrap_or_else(|| "main".to_string());
resolve_actor_identifier::<ApubCommunity, Community>(&name, &context, &local_user_view, true)
resolve_ap_identifier::<ApubCommunity, Community>(&name, &context, &local_user_view, true)
.await?
.id
}
@@ -57,7 +57,7 @@ pub async fn get_community(
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
let site = read_site_for_actor(community_view.community.actor_id.clone(), &context).await?;
let site = read_site_for_actor(community_view.community.ap_id.clone(), &context).await?;
let community_id = community_view.community.id;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;

View File

@@ -40,7 +40,7 @@ pub async fn read_person(
)
.await?;
let site = read_site_for_actor(person_view.person.actor_id.clone(), &context).await?;
let site = read_site_for_actor(person_view.person.ap_id.clone(), &context).await?;
Ok(Json(GetPersonDetailsResponse {
person_view,

View File

@@ -1,4 +1,4 @@
use crate::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
use crate::{fetcher::resolve_ap_identifier, objects::community::ApubCommunity};
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use lemmy_api_common::{
@@ -25,7 +25,7 @@ pub async fn search(
let community_id = if let Some(name) = &data.community_name {
Some(
resolve_actor_identifier::<ApubCommunity, Community>(name, &context, &local_user_view, false)
resolve_ap_identifier::<ApubCommunity, Community>(name, &context, &local_user_view, false)
.await?,
)
.map(|c| c.id)
@@ -33,6 +33,7 @@ pub async fn search(
data.community_id
};
let search_term = data.search_term.clone();
let time_range_seconds = data.time_range_seconds;
// parse pagination token
let page_after = if let Some(pa) = &data.page_cursor {
@@ -48,6 +49,7 @@ pub async fn search(
creator_id: data.creator_id,
type_: data.type_,
sort: data.sort,
time_range_seconds,
listing_type: data.listing_type,
title_only: data.title_only,
post_url_only: data.post_url_only,

View File

@@ -368,7 +368,7 @@ pub(crate) mod tests {
let follows = CommunityFollowerView::for_person(pool, import_user.person.id).await?;
assert_eq!(follows.len(), 1);
assert_eq!(follows[0].community.actor_id, community.actor_id);
assert_eq!(follows[0].community.ap_id, community.ap_id);
Person::delete(pool, export_user.person.id).await?;
Person::delete(pool, import_user.person.id).await?;

View File

@@ -41,7 +41,7 @@ impl Collection for ApubCommunityFeatured {
.await?;
Ok(GroupFeatured {
r#type: OrderedCollectionType::OrderedCollection,
id: generate_featured_url(&owner.actor_id)?.into(),
id: generate_featured_url(&owner.ap_id)?.into(),
total_items: ordered_items.len() as i32,
ordered_items,
})

View File

@@ -33,7 +33,7 @@ impl Collection for ApubCommunityFollower {
CommunityFollowerView::count_community_followers(&mut context.pool(), community_id).await?;
Ok(GroupFollowers {
id: generate_followers_url(&community.actor_id)?.into(),
id: generate_followers_url(&community.ap_id)?.into(),
r#type: CollectionType::Collection,
total_items: community_followers as i32,
items: vec![],

View File

@@ -32,11 +32,11 @@ impl Collection for ApubCommunityModerators {
let moderators = CommunityModeratorView::for_community(&mut data.pool(), owner.id).await?;
let ordered_items = moderators
.into_iter()
.map(|m| ObjectId::<ApubPerson>::from(m.moderator.actor_id))
.map(|m| ObjectId::<ApubPerson>::from(m.moderator.ap_id))
.collect();
Ok(GroupModerators {
r#type: OrderedCollectionType::OrderedCollection,
id: generate_moderators_url(&owner.actor_id)?.into(),
id: generate_moderators_url(&owner.ap_id)?.into(),
ordered_items,
})
}
@@ -60,7 +60,7 @@ impl Collection for ApubCommunityModerators {
CommunityModeratorView::for_community(&mut data.pool(), community_id).await?;
// Remove old mods from database which arent in the moderators collection anymore
for mod_user in &current_moderators {
let mod_id = ObjectId::from(mod_user.moderator.actor_id.clone());
let mod_id = ObjectId::from(mod_user.moderator.ap_id.clone());
if !apub.ordered_items.contains(&mod_id) {
let community_moderator_form = CommunityModeratorForm {
community_id: mod_user.community.id,
@@ -77,8 +77,8 @@ impl Collection for ApubCommunityModerators {
if let Some(mod_user) = mod_user {
if !current_moderators
.iter()
.map(|c| c.moderator.actor_id.clone())
.any(|x| x == mod_user.actor_id)
.map(|c| c.moderator.ap_id.clone())
.any(|x| x == mod_user.ap_id)
{
let community_moderator_form = CommunityModeratorForm {
community_id: owner.id,
@@ -136,7 +136,7 @@ mod tests {
CommunityModerator::join(&mut context.pool(), &community_moderator_form).await?;
assert_eq!(site.actor_id.to_string(), "https://enterprise.lemmy.ml/");
assert_eq!(site.ap_id.to_string(), "https://enterprise.lemmy.ml/");
let json: GroupModerators =
file_to_json_object("assets/lemmy/collections/group_moderators.json")?;

View File

@@ -62,7 +62,7 @@ impl Collection for ApubCommunityOutbox {
Ok(GroupOutbox {
r#type: OrderedCollectionType::OrderedCollection,
id: generate_outbox_url(&owner.actor_id)?.into(),
id: generate_outbox_url(&owner.ap_id)?.into(),
total_items: ordered_items.len() as i32,
ordered_items,
})

View File

@@ -1,10 +1,7 @@
use super::{search::SearchableObjects, user_or_community::UserOrCommunity};
use crate::fetcher::post_or_comment::PostOrComment;
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use lemmy_api_common::{
context::LemmyContext,
utils::{generate_local_apub_endpoint, EndpointType},
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{newtypes::InstanceId, source::instance::Instance};
use lemmy_utils::{
error::LemmyResult,
@@ -61,12 +58,8 @@ pub(crate) async fn to_local_url(url: &str, context: &Data<LemmyContext>) -> Opt
let dereferenced = object_id.dereference(context).await.ok()?;
match dereferenced {
SearchableObjects::PostOrComment(pc) => match *pc {
PostOrComment::Post(post) => {
generate_local_apub_endpoint(EndpointType::Post, &post.id.to_string(), local_domain)
}
PostOrComment::Comment(comment) => {
generate_local_apub_endpoint(EndpointType::Comment, &comment.id.to_string(), local_domain)
}
PostOrComment::Post(post) => post.local_url(context.settings()),
PostOrComment::Comment(comment) => comment.local_url(context.settings()),
}
.ok()
.map(Into::into),
@@ -150,7 +143,7 @@ mod tests {
),
(
"rewrite community link",
format!("[link]({})", community.actor_id),
format!("[link]({})", community.ap_id),
"[link](https://lemmy-alpha/c/my_community@example.com)",
),
(

View File

@@ -20,7 +20,7 @@ pub mod user_or_community;
///
/// In case the requesting user is logged in and the object was not found locally, it is attempted
/// to fetch via webfinger from the original instance.
pub async fn resolve_actor_identifier<ActorType, DbActor>(
pub async fn resolve_ap_identifier<ActorType, DbActor>(
identifier: &str,
context: &Data<LemmyContext>,
local_user_view: &Option<LocalUserView>,

View File

@@ -49,7 +49,7 @@ pub(crate) async fn get_apub_community_http(
.into();
if community.deleted || community.removed {
return create_apub_tombstone_response(community.actor_id.clone());
return create_apub_tombstone_response(community.ap_id.clone());
}
check_community_fetchable(&community)?;

View File

@@ -32,7 +32,7 @@ pub(crate) async fn get_apub_person_http(
create_apub_response(&apub)
} else {
create_apub_tombstone_response(person.actor_id.clone())
create_apub_tombstone_response(person.ap_id.clone())
}
}
@@ -43,7 +43,7 @@ pub(crate) async fn get_apub_person_outbox(
let person = Person::read_from_name(&mut context.pool(), &info.user_name, false)
.await?
.ok_or(LemmyErrorType::NotFound)?;
let outbox_id = generate_outbox_url(&person.actor_id)?.into();
let outbox_id = generate_outbox_url(&person.ap_id)?.into();
let outbox = EmptyOutbox::new(outbox_id)?;
create_apub_response(&outbox)
}

View File

@@ -51,7 +51,7 @@ pub async fn collect_non_local_mentions(
// Add the mention tag
let parent_creator_tag = Mention {
href: parent_creator.actor_id.clone().into(),
href: parent_creator.ap_id.clone().into(),
name: Some(format!(
"@{}@{}",
&parent_creator.name,
@@ -74,7 +74,7 @@ pub async fn collect_non_local_mentions(
let identifier = format!("{}@{}", mention.name, mention.domain);
let person = webfinger_resolve_actor::<LemmyContext, ApubPerson>(&identifier, context).await;
if let Ok(person) = person {
addressed_ccs.push(person.actor_id.to_string().parse()?);
addressed_ccs.push(person.ap_id.to_string().parse()?);
let mention_tag = Mention {
href: person.id(),

View File

@@ -110,7 +110,7 @@ impl Object for ApubComment {
let note = Note {
r#type: NoteType::Note,
id: self.ap_id.clone().into(),
attributed_to: creator.actor_id.into(),
attributed_to: creator.ap_id.into(),
to: generate_to(&community)?,
cc: maa.ccs,
content: markdown_to_html(&self.content),

View File

@@ -109,9 +109,9 @@ impl Object for ApubCommunity {
icon: self.icon.clone().map(ImageObject::new),
image: self.banner.clone().map(ImageObject::new),
sensitive: Some(self.nsfw),
featured: Some(generate_featured_url(&self.actor_id)?.into()),
featured: Some(generate_featured_url(&self.ap_id)?.into()),
inbox: self.inbox_url.clone().into(),
outbox: generate_outbox_url(&self.actor_id)?.into(),
outbox: generate_outbox_url(&self.ap_id)?.into(),
followers: self.followers_url.clone().map(Into::into),
endpoints: None,
public_key: self.public_key(),
@@ -119,7 +119,7 @@ impl Object for ApubCommunity {
published: Some(self.published),
updated: self.updated,
posting_restricted_to_mods: Some(self.posting_restricted_to_mods),
attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()),
attributed_to: Some(generate_moderators_url(&self.ap_id)?.into()),
manually_approves_followers: Some(self.visibility == CommunityVisibility::Private),
};
Ok(group)
@@ -134,7 +134,6 @@ impl Object for ApubCommunity {
}
/// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
async fn from_json(group: Group, context: &Data<Self::DataType>) -> LemmyResult<ApubCommunity> {
let instance_id = fetch_instance_actor_for_object(&group.id, context).await?;
@@ -156,7 +155,7 @@ impl Object for ApubCommunity {
updated: group.updated,
deleted: Some(false),
nsfw: Some(group.sensitive.unwrap_or(false)),
actor_id: Some(group.id.into()),
ap_id: Some(group.id.into()),
local: Some(false),
last_refreshed_at: Some(Utc::now()),
icon,
@@ -214,7 +213,7 @@ impl Object for ApubCommunity {
impl Actor for ApubCommunity {
fn id(&self) -> Url {
self.actor_id.inner().clone()
self.ap_id.inner().clone()
}
fn public_key_pem(&self) -> &str {

View File

@@ -108,7 +108,7 @@ impl Object for ApubSite {
icon: self.icon.clone().map(ImageObject::new),
image: self.banner.clone().map(ImageObject::new),
inbox: self.inbox_url.clone().into(),
outbox: Url::parse(&format!("{}site_outbox", self.actor_id))?,
outbox: Url::parse(&format!("{}site_outbox", self.ap_id))?,
public_key: self.public_key(),
language,
content_warning: self.content_warning.clone(),
@@ -159,7 +159,7 @@ impl Object for ApubSite {
icon,
banner,
description: apub.summary,
actor_id: Some(apub.id.clone().into()),
ap_id: Some(apub.id.clone().into()),
last_refreshed_at: Some(Utc::now()),
inbox_url: Some(apub.inbox.clone().into()),
public_key: Some(apub.public_key.public_key_pem.clone()),
@@ -178,7 +178,7 @@ impl Object for ApubSite {
impl Actor for ApubSite {
fn id(&self) -> Url {
self.actor_id.inner().clone()
self.ap_id.inner().clone()
}
fn public_key_pem(&self) -> &str {
@@ -205,7 +205,7 @@ pub(in crate::objects) async fn fetch_instance_actor_for_object<T: Into<Url> + C
context: &Data<LemmyContext>,
) -> LemmyResult<InstanceId> {
let object_id: Url = object_id.clone().into();
let instance_id = Site::instance_actor_id_from_url(object_id);
let instance_id = Site::instance_ap_id_from_url(object_id);
let site = ObjectId::<ApubSite>::from(instance_id.clone())
.dereference(context)
.await;

View File

@@ -100,7 +100,7 @@ impl Object for ApubPerson {
let person = Person {
kind,
id: self.actor_id.clone().into(),
id: self.ap_id.clone().into(),
preferred_username: self.name.clone(),
name: self.display_name.clone(),
summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
@@ -109,7 +109,7 @@ impl Object for ApubPerson {
image: self.banner.clone().map(ImageObject::new),
matrix_user_id: self.matrix_user_id.clone(),
published: Some(self.published),
outbox: generate_outbox_url(&self.actor_id)?.into(),
outbox: generate_outbox_url(&self.ap_id)?.into(),
endpoints: None,
public_key: self.public_key(),
updated: self.updated,
@@ -163,7 +163,7 @@ impl Object for ApubPerson {
banner,
published: person.published,
updated: person.updated,
actor_id: Some(person.id.into()),
ap_id: Some(person.id.into()),
bio,
local: Some(false),
bot_account: Some(person.kind == UserTypes::Service),
@@ -188,7 +188,7 @@ impl Object for ApubPerson {
impl Actor for ApubPerson {
fn id(&self) -> Url {
self.actor_id.inner().clone()
self.ap_id.inner().clone()
}
fn public_key_pem(&self) -> &str {
@@ -268,7 +268,7 @@ pub(crate) mod tests {
ApubPerson::verify(&json, &url, &context).await?;
let person = ApubPerson::from_json(json, &context).await?;
assert_eq!(person.actor_id, url.into());
assert_eq!(person.ap_id, url.into());
assert_eq!(person.name, "lanodan");
assert!(!person.local);
assert_eq!(context.request_count(), 0);

View File

@@ -133,7 +133,7 @@ impl Object for ApubPost {
let page = Page {
kind: PageType::Page,
id: self.ap_id.clone().into(),
attributed_to: AttributedTo::Lemmy(creator.actor_id.into()),
attributed_to: AttributedTo::Lemmy(creator.ap_id.into()),
to: generate_to(&community)?,
cc: vec![],
name: Some(self.name.clone()),

View File

@@ -107,8 +107,8 @@ impl Object for ApubPrivateMessage {
let note = PrivateMessage {
kind,
id: self.ap_id.clone().into(),
attributed_to: creator.actor_id.into(),
to: [recipient.actor_id.into()],
attributed_to: creator.ap_id.into(),
to: [recipient.ap_id.into()],
content: markdown_to_html(&self.content),
media_type: Some(MediaTypeHtml::Html),
source: Some(Source::new(self.content.clone())),
@@ -131,7 +131,7 @@ impl Object for ApubPrivateMessage {
let person = note.attributed_to.dereference(context).await?;
if person.banned {
Err(FederationError::PersonIsBannedFromSite(
person.actor_id.to_string(),
person.ap_id.to_string(),
))?
} else {
Ok(())

View File

@@ -192,7 +192,7 @@ fn site() -> LemmyResult<Site> {
icon: None,
banner: None,
description: None,
actor_id: Url::parse("http://example.com")?.into(),
ap_id: Url::parse("http://example.com")?.into(),
last_refreshed_at: Default::default(),
inbox_url: Url::parse("http://example.com")?.into(),
private_key: None,

View File

@@ -67,7 +67,7 @@ regex = { workspace = true, optional = true }
diesel_ltree = { workspace = true, optional = true }
async-trait = { workspace = true }
tracing = { workspace = true }
deadpool = { version = "0.12.1", optional = true, features = ["rt_tokio_1"] }
deadpool = { version = "0.12.2", optional = true, features = ["rt_tokio_1"] }
ts-rs = { workspace = true, optional = true }
futures-util = { workspace = true }
tokio = { workspace = true, optional = true }

View File

@@ -392,10 +392,11 @@ BEGIN
report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count
FROM (
SELECT
(post_report).post_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (post_report).resolved), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (post_report).post_id) AS diff
(post_report).post_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (post_report).resolved
AND NOT (post_report).violates_instance_rules), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (post_report).post_id) AS diff
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
AND a.post_id = diff.post_id;
AND a.post_id = diff.post_id;
RETURN NULL;
@@ -411,10 +412,11 @@ BEGIN
report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count
FROM (
SELECT
(comment_report).comment_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (comment_report).resolved), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (comment_report).comment_id) AS diff
(comment_report).comment_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (comment_report).resolved
AND NOT (comment_report).violates_instance_rules), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (comment_report).comment_id) AS diff
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
AND a.comment_id = diff.comment_id;
AND a.comment_id = diff.comment_id;
RETURN NULL;

View File

@@ -70,14 +70,14 @@ pub struct CommunityAggregates {
pub users_active_month: i64,
/// The number of users with any activity in the last year.
pub users_active_half_year: i64,
/// Number of any interactions over the last month.
#[serde(skip)]
pub interactions_month: i64,
#[serde(skip)]
pub hot_rank: f64,
pub subscribers_local: i64,
pub report_count: i16,
pub unresolved_report_count: i16,
/// Number of any interactions over the last month.
#[serde(skip)]
pub interactions_month: i64,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
@@ -99,6 +99,7 @@ pub struct PersonAggregates {
pub comment_count: i64,
#[serde(skip)]
pub comment_score: i64,
pub published: DateTime<Utc>,
}
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]

View File

@@ -1,5 +1,6 @@
use crate::{
diesel::{DecoratableTarget, OptionalExtension},
impls::local_user::local_user_can_mod,
newtypes::{CommentId, DbUrl, PersonId},
schema::{comment, comment_actions},
source::comment::{
@@ -16,15 +17,17 @@ use crate::{
};
use chrono::{DateTime, Utc};
use diesel::{
dsl::insert_into,
dsl::{case_when, insert_into, not},
expression::SelectableHelper,
result::Error,
BoolExpressionMethods,
ExpressionMethods,
NullableExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;
use diesel_ltree::Ltree;
use lemmy_utils::{error::LemmyResult, settings::structs::Settings};
use url::Url;
impl Comment {
@@ -116,6 +119,38 @@ impl Comment {
None
}
}
pub fn local_url(&self, settings: &Settings) -> LemmyResult<DbUrl> {
let domain = settings.get_protocol_and_hostname();
Ok(Url::parse(&format!("{domain}/comment/{}", self.id))?.into())
}
}
/// Selects the comment columns, but gives an empty string for content when
/// deleted or removed, and you're not a mod/admin.
#[diesel::dsl::auto_type]
pub fn comment_select_remove_deletes() -> _ {
let deleted_or_removed = comment::deleted.or(comment::removed);
// You can only view the content if it hasn't been removed, or you can mod.
let can_view_content = not(deleted_or_removed).or(local_user_can_mod());
let content = case_when(can_view_content, comment::content).otherwise("");
(
comment::id,
comment::creator_id,
comment::post_id,
content,
comment::removed,
comment::published,
comment::updated,
comment::deleted,
comment::ap_id,
comment::local,
comment::path,
comment::distinguished,
comment::language_id,
)
}
#[async_trait]

View File

@@ -33,7 +33,7 @@ use crate::{
use chrono::{DateTime, Utc};
use diesel::{
deserialize,
dsl::{self, exists, insert_into, not},
dsl::{exists, insert_into, not},
expression::SelectableHelper,
pg::Pg,
result::Error,
@@ -47,7 +47,11 @@ use diesel::{
Queryable,
};
use diesel_async::RunQueryDsl;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::{
error::{LemmyErrorType, LemmyResult},
settings::structs::Settings,
};
use url::Url;
#[async_trait]
impl Crud for Community {
@@ -133,7 +137,7 @@ impl Community {
timestamp: DateTime<Utc>,
form: &CommunityInsertForm,
) -> Result<Self, Error> {
let is_new_community = match &form.actor_id {
let is_new_community = match &form.ap_id {
Some(id) => Community::read_from_apub_id(pool, id).await?.is_none(),
None => true,
};
@@ -142,7 +146,7 @@ impl Community {
// Can't do separate insert/update commands because InsertForm/UpdateForm aren't convertible
let community_ = insert_into(community::table)
.values(form)
.on_conflict(community::actor_id)
.on_conflict(community::ap_id)
.filter_target(coalesce(community::updated, community::published).lt(timestamp))
.do_update()
.set(form)
@@ -273,6 +277,11 @@ impl Community {
.eq(false)
.and(community::deleted.eq(false))
}
pub fn local_url(name: &str, settings: &Settings) -> LemmyResult<DbUrl> {
let domain = settings.get_protocol_and_hostname();
Ok(Url::parse(&format!("{domain}/c/{name}"))?.into())
}
}
impl CommunityModerator {
@@ -389,10 +398,6 @@ impl Bannable for CommunityPersonBan {
}
impl CommunityFollower {
pub fn select_subscribed_type() -> dsl::Nullable<community_actions::follow_state> {
community_actions::follow_state.nullable()
}
/// Check if a remote instance has any followers on local instance. For this it is enough to check
/// if any follow relation is stored. Dont use this for local community.
pub async fn check_has_local_followers(
@@ -431,6 +436,14 @@ impl CommunityFollower {
}
}
// TODO
// I'd really like to have these on the impl, but unfortunately they have to be top level,
// according to https://diesel.rs/guides/composing-applications.html
#[diesel::dsl::auto_type]
pub fn community_follower_select_subscribed_type() -> _ {
community_actions::follow_state.nullable()
}
impl Queryable<sql_types::Nullable<crate::schema::sql_types::CommunityFollowerState>, Pg>
for SubscribedType
{
@@ -500,7 +513,7 @@ impl ApubActor for Community {
) -> Result<Option<Self>, Error> {
let conn = &mut get_conn(pool).await?;
community::table
.filter(community::actor_id.eq(object_id))
.filter(community::ap_id.eq(object_id))
.first(conn)
.await
.optional()
@@ -600,7 +613,7 @@ mod tests {
deleted: false,
published: inserted_community.published,
updated: None,
actor_id: inserted_community.actor_id.clone(),
ap_id: inserted_community.ap_id.clone(),
local: true,
private_key: None,
public_key: "pubkey".to_owned(),

View File

@@ -1,7 +1,7 @@
use crate::{
newtypes::{DbUrl, LocalUserId},
schema::{image_details, local_image, remote_image},
source::images::{ImageDetails, ImageDetailsForm, LocalImage, LocalImageForm, RemoteImage},
source::images::{ImageDetails, ImageDetailsInsertForm, LocalImage, LocalImageForm, RemoteImage},
utils::{get_conn, DbPool},
};
use diesel::{
@@ -21,7 +21,7 @@ impl LocalImage {
pub async fn create(
pool: &mut DbPool<'_>,
form: &LocalImageForm,
image_details_form: &ImageDetailsForm,
image_details_form: &ImageDetailsInsertForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
conn
@@ -102,7 +102,10 @@ impl RemoteImage {
}
impl ImageDetails {
pub async fn create(pool: &mut DbPool<'_>, form: &ImageDetailsForm) -> Result<usize, Error> {
pub async fn create(
pool: &mut DbPool<'_>,
form: &ImageDetailsInsertForm,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
insert_into(image_details::table)

View File

@@ -1,4 +1,5 @@
use crate::{
aliases::creator_community_actions,
newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId},
schema::{community, community_actions, local_user, person, registration_application},
source::{
@@ -19,13 +20,19 @@ use bcrypt::{hash, DEFAULT_COST};
use diesel::{
dsl::{insert_into, not, IntervalDsl},
result::Error,
BoolExpressionMethods,
CombineDsl,
ExpressionMethods,
JoinOnDsl,
NullableExpressionMethods,
PgExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use lemmy_utils::{
email::{lang_str_to_lang, translations::Lang},
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
};
impl LocalUser {
pub async fn create(
@@ -171,7 +178,7 @@ impl LocalUser {
.filter(community_actions::followed.is_not_null())
.filter(community_actions::person_id.eq(person_id_))
.inner_join(community::table)
.select(community::actor_id)
.select(community::ap_id)
.get_results(conn)
.await?;
@@ -195,7 +202,7 @@ impl LocalUser {
.filter(community_actions::blocked.is_not_null())
.filter(community_actions::person_id.eq(person_id_))
.inner_join(community::table)
.select(community::actor_id)
.select(community::ap_id)
.get_results(conn)
.await?;
@@ -203,7 +210,7 @@ impl LocalUser {
.filter(person_actions::blocked.is_not_null())
.filter(person_actions::person_id.eq(person_id_))
.inner_join(person::table.on(person_actions::target_id.eq(person::id)))
.select(person::actor_id)
.select(person::ap_id)
.get_results(conn)
.await?;
@@ -293,6 +300,33 @@ impl LocalUser {
Err(LemmyErrorType::NotHigherMod)?
}
}
pub fn interface_i18n_language(&self) -> Lang {
lang_str_to_lang(&self.interface_language)
}
}
// TODO
// I'd really like to have these on the impl, but unfortunately they have to be top level,
// according to https://diesel.rs/guides/composing-applications.html
/// Checks to see if you can mod an item.
///
/// Caveat: Since admin status isn't federated or ordered, it can't know whether
/// item creator is a federated admin, or a higher admin.
/// The back-end will reject an action for admin that is higher via
/// LocalUser::is_higher_mod_or_admin_check
#[diesel::dsl::auto_type]
pub fn local_user_can_mod() -> _ {
let am_admin = local_user::admin.nullable();
let creator_became_moderator = creator_community_actions
.field(community_actions::became_moderator)
.nullable();
let am_higher_mod = community_actions::became_moderator
.nullable()
.le(creator_became_moderator);
am_admin.or(am_higher_mod).is_not_distinct_from(true)
}
/// Adds some helper functions for an optional LocalUser

View File

@@ -24,7 +24,11 @@ use diesel::{
QueryDsl,
};
use diesel_async::RunQueryDsl;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::{
error::{LemmyErrorType, LemmyResult},
settings::structs::Settings,
};
use url::Url;
#[async_trait]
impl Crud for Person {
@@ -71,7 +75,7 @@ impl Person {
let conn = &mut get_conn(pool).await?;
insert_into(person::table)
.values(form)
.on_conflict(person::actor_id)
.on_conflict(person::ap_id)
.do_update()
.set(form)
.get_result::<Self>(conn)
@@ -138,6 +142,11 @@ impl Person {
.then_some(())
.ok_or(LemmyErrorType::UsernameAlreadyExists.into())
}
pub fn local_url(name: &str, settings: &Settings) -> LemmyResult<DbUrl> {
let domain = settings.get_protocol_and_hostname();
Ok(Url::parse(&format!("{domain}/u/{name}"))?.into())
}
}
impl PersonInsertForm {
@@ -155,7 +164,7 @@ impl ApubActor for Person {
let conn = &mut get_conn(pool).await?;
person::table
.filter(person::deleted.eq(false))
.filter(person::actor_id.eq(object_id))
.filter(person::ap_id.eq(object_id))
.first(conn)
.await
.optional()
@@ -282,7 +291,7 @@ mod tests {
deleted: false,
published: inserted_person.published,
updated: None,
actor_id: inserted_person.actor_id.clone(),
ap_id: inserted_person.ap_id.clone(),
bio: None,
local: true,
bot_account: false,
@@ -298,7 +307,7 @@ mod tests {
let read_person = Person::read(pool, inserted_person.id).await?;
let update_person_form = PersonUpdateForm {
actor_id: Some(inserted_person.actor_id.clone()),
ap_id: Some(inserted_person.ap_id.clone()),
..Default::default()
};
let updated_person = Person::update(pool, inserted_person.id, &update_person_form).await?;

View File

@@ -41,7 +41,10 @@ use diesel::{
TextExpressionMethods,
};
use diesel_async::RunQueryDsl;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
settings::structs::Settings,
};
#[async_trait]
impl Crud for Post {
@@ -270,6 +273,11 @@ impl Post {
.first::<i64>(conn)
.await
}
pub fn local_url(&self, settings: &Settings) -> LemmyResult<DbUrl> {
let domain = settings.get_protocol_and_hostname();
Ok(Url::parse(&format!("{domain}/post/{}", self.id))?.into())
}
}
#[async_trait]

View File

@@ -9,6 +9,7 @@ use crate::{
use chrono::{DateTime, Utc};
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_utils::{error::LemmyResult, settings::structs::Settings};
use url::Url;
#[async_trait]
@@ -82,6 +83,25 @@ impl PrivateMessage {
.await
.optional()
}
pub fn local_url(&self, settings: &Settings) -> LemmyResult<DbUrl> {
let domain = settings.get_protocol_and_hostname();
Ok(Url::parse(&format!("{domain}/private_message/{}", self.id))?.into())
}
pub async fn update_removed_for_creator(
pool: &mut DbPool<'_>,
for_creator_id: PersonId,
removed: bool,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(private_message::table.filter(private_message::creator_id.eq(for_creator_id)))
.set((
private_message::removed.eq(removed),
private_message::updated.eq(Utc::now()),
))
.get_results::<Self>(conn)
.await
}
}
#[cfg(test)]
@@ -140,6 +160,7 @@ mod tests {
))?
.into(),
local: true,
removed: false,
};
let read_private_message = PrivateMessage::read(pool, inserted_private_message.id).await?;

View File

@@ -25,7 +25,7 @@ impl Crud for Site {
}
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result<Self, Error> {
let is_new_site = match &form.actor_id {
let is_new_site = match &form.ap_id {
Some(id_) => Site::read_from_apub_id(pool, id_).await?.is_none(),
None => true,
};
@@ -34,7 +34,7 @@ impl Crud for Site {
// Can't do separate insert/update commands because InsertForm/UpdateForm aren't convertible
let site_ = insert_into(site::table)
.values(form)
.on_conflict(site::actor_id)
.on_conflict(site::ap_id)
.do_update()
.set(form)
.get_result::<Self>(conn)
@@ -80,7 +80,7 @@ impl Site {
let conn = &mut get_conn(pool).await?;
site::table
.filter(site::actor_id.eq(object_id))
.filter(site::ap_id.eq(object_id))
.first(conn)
.await
.optional()
@@ -97,7 +97,7 @@ impl Site {
/// Instance actor is at the root path, so we simply need to clear the path and other unnecessary
/// parts of the url.
pub fn instance_actor_id_from_url(mut url: Url) -> Url {
pub fn instance_ap_id_from_url(mut url: Url) -> Url {
url.set_fragment(None);
url.set_path("");
url.set_query(None);

View File

@@ -23,9 +23,10 @@ pub mod sensitive;
pub mod schema;
#[cfg(feature = "full")]
pub mod aliases {
use crate::schema::{community_actions, person};
use crate::schema::{community_actions, local_user, person};
diesel::alias!(
community_actions as creator_community_actions: CreatorCommunityActions,
local_user as creator_local_user: CreatorLocalUser,
person as person1: Person1,
person as person2: Person2,
);
@@ -66,19 +67,9 @@ pub enum PostSortType {
Hot,
New,
Old,
TopDay,
TopWeek,
TopMonth,
TopYear,
TopAll,
Top,
MostComments,
NewComments,
TopHour,
TopSixHour,
TopTwelveHour,
TopThreeMonths,
TopSixMonths,
TopNineMonths,
Controversial,
Scaled,
}
@@ -341,7 +332,7 @@ pub type Person1AliasAllColumnsTuple = (
AliasedField<aliases::Person1, person::banned>,
AliasedField<aliases::Person1, person::published>,
AliasedField<aliases::Person1, person::updated>,
AliasedField<aliases::Person1, person::actor_id>,
AliasedField<aliases::Person1, person::ap_id>,
AliasedField<aliases::Person1, person::bio>,
AliasedField<aliases::Person1, person::local>,
AliasedField<aliases::Person1, person::private_key>,

View File

@@ -179,6 +179,7 @@ diesel::table! {
resolver_id -> Nullable<Int4>,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
violates_instance_rules -> Bool,
}
}
@@ -199,7 +200,7 @@ diesel::table! {
deleted -> Bool,
nsfw -> Bool,
#[max_length = 255]
actor_id -> Varchar,
ap_id -> Varchar,
local -> Bool,
private_key -> Nullable<Text>,
public_key -> Text,
@@ -252,11 +253,11 @@ diesel::table! {
users_active_week -> Int8,
users_active_month -> Int8,
users_active_half_year -> Int8,
interactions_month -> Int8,
hot_rank -> Float8,
subscribers_local -> Int8,
report_count -> Int2,
unresolved_report_count -> Int2,
interactions_month -> Int8,
}
}
@@ -350,6 +351,8 @@ diesel::table! {
width -> Int4,
height -> Int4,
content_type -> Text,
#[max_length = 50]
blurhash -> Nullable<Varchar>,
}
}
@@ -445,6 +448,7 @@ diesel::table! {
comment_upvotes -> FederationModeEnum,
comment_downvotes -> FederationModeEnum,
disable_donation_dialog -> Bool,
default_post_time_range_seconds -> Nullable<Int4>,
}
}
@@ -518,6 +522,7 @@ diesel::table! {
auto_mark_fetched_posts_as_read -> Bool,
last_donation_notification -> Timestamptz,
hide_media -> Bool,
default_post_time_range_seconds -> Nullable<Int4>,
}
}
@@ -745,7 +750,7 @@ diesel::table! {
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
#[max_length = 255]
actor_id -> Varchar,
ap_id -> Varchar,
bio -> Nullable<Text>,
local -> Bool,
private_key -> Nullable<Text>,
@@ -779,6 +784,7 @@ diesel::table! {
post_score -> Int8,
comment_count -> Int8,
comment_score -> Int8,
published -> Timestamptz,
}
}
@@ -912,6 +918,7 @@ diesel::table! {
resolver_id -> Nullable<Int4>,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
violates_instance_rules -> Bool,
}
}
@@ -943,6 +950,7 @@ diesel::table! {
#[max_length = 255]
ap_id -> Varchar,
local -> Bool,
removed -> Bool,
}
}
@@ -1046,7 +1054,7 @@ diesel::table! {
#[max_length = 150]
description -> Nullable<Varchar>,
#[max_length = 255]
actor_id -> Varchar,
ap_id -> Varchar,
last_refreshed_at -> Timestamptz,
#[max_length = 255]
inbox_url -> Varchar,

View File

@@ -30,6 +30,7 @@ pub struct CommentReport {
pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub updated: Option<DateTime<Utc>>,
pub violates_instance_rules: bool,
}
#[derive(Clone)]
@@ -40,4 +41,5 @@ pub struct CommentReportForm {
pub comment_id: CommentId,
pub original_comment_text: String,
pub reason: String,
pub violates_instance_rules: bool,
}

View File

@@ -39,8 +39,8 @@ pub struct Community {
pub deleted: bool,
/// Whether its an NSFW community.
pub nsfw: bool,
/// The federated actor_id.
pub actor_id: DbUrl,
/// The federated ap_id.
pub ap_id: DbUrl,
/// Whether the community is local.
pub local: bool,
#[serde(skip)]
@@ -101,7 +101,7 @@ pub struct CommunityInsertForm {
#[new(default)]
pub nsfw: Option<bool>,
#[new(default)]
pub actor_id: Option<DbUrl>,
pub ap_id: Option<DbUrl>,
#[new(default)]
pub local: Option<bool>,
#[new(default)]
@@ -141,7 +141,7 @@ pub struct CommunityUpdateForm {
pub updated: Option<Option<DateTime<Utc>>>,
pub deleted: Option<bool>,
pub nsfw: Option<bool>,
pub actor_id: Option<DbUrl>,
pub ap_id: Option<DbUrl>,
pub local: Option<bool>,
pub public_key: Option<String>,
pub private_key: Option<Option<String>>,

View File

@@ -62,14 +62,17 @@ pub struct ImageDetails {
pub width: i32,
pub height: i32,
pub content_type: String,
#[cfg_attr(feature = "full", ts(optional))]
pub blurhash: Option<String>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = image_details))]
pub struct ImageDetailsForm {
pub struct ImageDetailsInsertForm {
pub link: DbUrl,
pub width: i32,
pub height: i32,
pub content_type: String,
pub blurhash: Option<String>,
}

View File

@@ -86,6 +86,9 @@ pub struct LocalSite {
/// If this is true, users will never see the dialog asking to support Lemmy development with
/// donations.
pub disable_donation_dialog: bool,
#[cfg_attr(feature = "full", ts(optional))]
/// A default time range limit to apply to post sorts, in seconds.
pub default_post_time_range_seconds: Option<i32>,
}
#[derive(Clone, derive_new::new)]
@@ -147,6 +150,8 @@ pub struct LocalSiteInsertForm {
pub comment_downvotes: Option<FederationMode>,
#[new(default)]
pub disable_donation_dialog: Option<bool>,
#[new(default)]
pub default_post_time_range_seconds: Option<Option<i32>>,
}
#[derive(Clone, Default)]
@@ -181,4 +186,5 @@ pub struct LocalSiteUpdateForm {
pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>,
pub disable_donation_dialog: Option<bool>,
pub default_post_time_range_seconds: Option<Option<i32>>,
}

View File

@@ -76,6 +76,9 @@ pub struct LocalUser {
pub last_donation_notification: DateTime<Utc>,
/// Whether to hide posts containing images/videos
pub hide_media: bool,
#[cfg_attr(feature = "full", ts(optional))]
/// A default time range limit to apply to post sorts, in seconds.
pub default_post_time_range_seconds: Option<i32>,
}
#[derive(Clone, derive_new::new)]
@@ -138,6 +141,8 @@ pub struct LocalUserInsertForm {
pub last_donation_notification: Option<DateTime<Utc>>,
#[new(default)]
pub hide_media: Option<bool>,
#[new(default)]
pub default_post_time_range_seconds: Option<Option<i32>>,
}
#[derive(Clone, Default)]
@@ -172,4 +177,5 @@ pub struct LocalUserUpdateForm {
pub auto_mark_fetched_posts_as_read: Option<bool>,
pub last_donation_notification: Option<DateTime<Utc>>,
pub hide_media: Option<bool>,
pub default_post_time_range_seconds: Option<Option<i32>>,
}

View File

@@ -34,8 +34,8 @@ pub struct Person {
pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub updated: Option<DateTime<Utc>>,
/// The federated actor_id.
pub actor_id: DbUrl,
/// The federated ap_id.
pub ap_id: DbUrl,
/// An optional bio, in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub bio: Option<String>,
@@ -84,7 +84,7 @@ pub struct PersonInsertForm {
#[new(default)]
pub updated: Option<DateTime<Utc>>,
#[new(default)]
pub actor_id: Option<DbUrl>,
pub ap_id: Option<DbUrl>,
#[new(default)]
pub bio: Option<String>,
#[new(default)]
@@ -115,7 +115,7 @@ pub struct PersonUpdateForm {
pub avatar: Option<Option<DbUrl>>,
pub banned: Option<bool>,
pub updated: Option<Option<DateTime<Utc>>>,
pub actor_id: Option<DbUrl>,
pub ap_id: Option<DbUrl>,
pub bio: Option<Option<String>>,
pub local: Option<bool>,
pub public_key: Option<String>,

View File

@@ -37,6 +37,7 @@ pub struct PostReport {
pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub updated: Option<DateTime<Utc>>,
pub violates_instance_rules: bool,
}
#[derive(Clone, Default)]
@@ -49,4 +50,5 @@ pub struct PostReportForm {
pub original_post_url: Option<DbUrl>,
pub original_post_body: Option<String>,
pub reason: String,
pub violates_instance_rules: bool,
}

View File

@@ -33,6 +33,7 @@ pub struct PrivateMessage {
pub updated: Option<DateTime<Utc>>,
pub ap_id: DbUrl,
pub local: bool,
pub removed: bool,
}
#[derive(Clone, derive_new::new)]
@@ -67,4 +68,5 @@ pub struct PrivateMessageUpdateForm {
pub updated: Option<Option<DateTime<Utc>>>,
pub ap_id: Option<DbUrl>,
pub local: Option<bool>,
pub removed: Option<bool>,
}

View File

@@ -35,8 +35,8 @@ pub struct Site {
/// A shorter, one-line description of the site.
#[cfg_attr(feature = "full", ts(optional))]
pub description: Option<String>,
/// The federated actor_id.
pub actor_id: DbUrl,
/// The federated ap_id.
pub ap_id: DbUrl,
/// The time the site was last refreshed.
pub last_refreshed_at: DateTime<Utc>,
/// The site inbox
@@ -69,7 +69,7 @@ pub struct SiteInsertForm {
#[new(default)]
pub description: Option<String>,
#[new(default)]
pub actor_id: Option<DbUrl>,
pub ap_id: Option<DbUrl>,
#[new(default)]
pub last_refreshed_at: Option<DateTime<Utc>>,
#[new(default)]
@@ -94,7 +94,7 @@ pub struct SiteUpdateForm {
pub icon: Option<Option<DbUrl>>,
pub banner: Option<Option<DbUrl>>,
pub description: Option<Option<String>>,
pub actor_id: Option<DbUrl>,
pub ap_id: Option<DbUrl>,
pub last_refreshed_at: Option<DateTime<Utc>>,
pub inbox_url: Option<DbUrl>,
pub private_key: Option<Option<String>>,

View File

@@ -1,13 +1,13 @@
pub mod uplete;
use crate::{newtypes::DbUrl, schema_setup, CommentSortType, PostSortType};
use crate::{newtypes::DbUrl, schema_setup};
use chrono::TimeDelta;
use deadpool::Runtime;
use diesel::{
dsl,
expression::AsExpression,
helper_types::AsExprOf,
pg::Pg,
pg::{data_types::PgInterval, Pg},
query_builder::{Query, QueryFragment},
query_dsl::methods::LimitDsl,
result::{
@@ -303,6 +303,16 @@ pub fn diesel_string_update(opt: Option<&str>) -> Option<Option<String>> {
}
}
/// Takes an API optional number, and converts it to an optional diesel DB update. Zero means erase.
pub fn diesel_opt_number_update(opt: Option<i32>) -> Option<Option<i32>> {
match opt {
// Zero is an erase
Some(0) => Some(None),
Some(num) => Some(Some(num)),
None => None,
}
}
/// Takes an API optional text input, and converts it to an optional diesel DB update (for non
/// nullable properties).
pub fn diesel_required_string_update(opt: Option<&str>) -> Option<String> {
@@ -495,18 +505,6 @@ pub fn build_db_pool_for_tests() -> ActualDbPool {
build_db_pool().expect("db pool missing")
}
pub fn post_to_comment_sort_type(sort: PostSortType) -> CommentSortType {
use PostSortType::*;
match sort {
Active | Hot | Scaled => CommentSortType::Hot,
New | NewComments | MostComments => CommentSortType::New,
Old => CommentSortType::Old,
Controversial => CommentSortType::Controversial,
TopHour | TopSixHour | TopTwelveHour | TopDay | TopAll | TopWeek | TopYear | TopMonth
| TopThreeMonths | TopSixMonths | TopNineMonths => CommentSortType::Top,
}
}
#[allow(clippy::expect_used)]
static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
@@ -557,6 +555,10 @@ pub fn now() -> AsExprOf<diesel::dsl::now, diesel::sql_types::Timestamptz> {
diesel::dsl::now.into_sql::<Timestamptz>()
}
pub fn seconds_to_pg_interval(seconds: i32) -> PgInterval {
PgInterval::from_microseconds(i64::from(seconds) * 1_000_000)
}
/// Trait alias for a type that can be converted to an SQL tuple using `IntoSql::into_sql`
pub trait AsRecord: Expression + AsExpression<sql_types::Record<Self::SqlType>>
where

View File

@@ -48,4 +48,4 @@ serial_test = { workspace = true }
tokio = { workspace = true }
pretty_assertions = { workspace = true }
url = { workspace = true }
test-context = "0.3.0"
test-context = "0.4.1"

Some files were not shown because too many files have changed in this diff Show More