Merge remote-tracking branch 'origin/main' into persistent-queue

This commit is contained in:
phiresky 2023-07-30 18:21:08 +00:00
commit 11e4c450ba
128 changed files with 3273 additions and 3585 deletions

2
.github/CODEOWNERS vendored
View File

@ -1 +1,3 @@
* @Nutomic @dessalines @phiresky
crates/apub/ @Nutomic
migrations/ @dessalines @phiresky

3
.gitignore vendored
View File

@ -30,3 +30,6 @@ bindings
# Database cluster and sockets for testing
dev_pgdata/
*.PGSQL.*
# database dumps
*.sqldump

View File

@ -1 +0,0 @@
*.sqldump

357
Cargo.lock generated
View File

@ -135,7 +135,7 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rand 0.8.5",
"rand",
"sha1",
"smallvec",
"tokio",
@ -346,7 +346,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.10",
"getrandom",
"once_cell",
"version_check",
]
@ -358,7 +358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom 0.2.10",
"getrandom",
"once_cell",
"version_check",
]
@ -387,6 +387,19 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "ammonia"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e6d1c7838db705c9b756557ee27c384ce695a1c51a6fe528784cb1c6840170"
dependencies = [
"html5ever",
"maplit",
"once_cell",
"tendril",
"url",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -466,12 +479,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "assert-json-diff"
version = "2.0.2"
@ -482,6 +489,19 @@ dependencies = [
"serde_json",
]
[[package]]
name = "async-compression"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b74f44609f0f91493e3082d3734d98497e094777144380ea4db9f9905dd5b6"
dependencies = [
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-io"
version = "1.13.0"
@ -588,7 +608,7 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rand 0.8.5",
"rand",
"rustls 0.20.7",
"serde",
"serde_json",
@ -727,7 +747,7 @@ checksum = "28d1c9c15093eb224f0baa400f38fcd713fc1391a6f1c389d886beef146d60a3"
dependencies = [
"base64 0.21.2",
"blowfish",
"getrandom 0.2.10",
"getrandom",
"subtle",
"zeroize",
]
@ -768,18 +788,6 @@ version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
[[package]]
name = "bitvec"
version = "0.19.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.3"
@ -878,7 +886,7 @@ dependencies = [
"hound",
"image",
"lodepng",
"rand 0.8.5",
"rand",
"serde_json",
]
@ -1078,7 +1086,7 @@ dependencies = [
"async-trait",
"json5",
"lazy_static",
"nom 7.1.1",
"nom",
"pathdiff",
"ron",
"rust-ini",
@ -1790,7 +1798,7 @@ checksum = "2e1f6c3800b304a6be0012039e2a45a322a093539c45ab818d9e6895a39c90fe"
dependencies = [
"proc-macro2",
"quote",
"rand 0.8.5",
"rand",
"syn 1.0.103",
]
@ -1941,12 +1949,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futf"
version = "0.1.5"
@ -2080,17 +2082,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.10"
@ -2165,7 +2156,7 @@ dependencies = [
"base64 0.13.1",
"byteorder",
"flate2",
"nom 7.1.1",
"nom",
"num-traits",
]
@ -2237,10 +2228,10 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be92446e11d68f5d71367d571c229d09ced1f24ab6d08ea0bff329d5f6c0b2a3"
dependencies = [
"html5ever 0.26.0",
"html5ever",
"jni",
"lazy_static",
"markup5ever_rcdom 0.2.0",
"markup5ever_rcdom",
"percent-encoding",
"regex",
]
@ -2251,25 +2242,11 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74cda84f06c1cc83476f79ae8e2e892b626bdadafcb227baec54c918cadc18a0"
dependencies = [
"html5ever 0.26.0",
"markup5ever 0.11.0",
"html5ever",
"markup5ever",
"tendril",
"unicode-width",
"xml5ever 0.17.0",
]
[[package]]
name = "html5ever"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
dependencies = [
"log",
"mac",
"markup5ever 0.10.1",
"proc-macro2",
"quote",
"syn 1.0.103",
"xml5ever",
]
[[package]]
@ -2280,7 +2257,7 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever 0.11.0",
"markup5ever",
"proc-macro2",
"quote",
"syn 1.0.103",
@ -2646,6 +2623,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
name = "lemmy_api"
version = "0.18.1"
dependencies = [
"activitypub_federation",
"actix-web",
"anyhow",
"async-trait",
@ -2673,11 +2651,12 @@ version = "0.18.1"
dependencies = [
"activitypub_federation",
"actix-web",
"ammonia",
"anyhow",
"chrono",
"encoding",
"futures",
"getrandom 0.2.10",
"getrandom",
"lemmy_db_schema",
"lemmy_db_views",
"lemmy_db_views_actor",
@ -2998,7 +2977,7 @@ dependencies = [
"idna 0.3.0",
"mime",
"native-tls",
"nom 7.1.1",
"nom",
"once_cell",
"quoted_printable",
"socket2 0.4.9",
@ -3006,19 +2985,6 @@ dependencies = [
"tokio-native-tls",
]
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec",
"bitflags 1.3.2",
"cfg-if",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.146"
@ -3141,6 +3107,12 @@ dependencies = [
"libc",
]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markdown-it"
version = "0.5.1"
@ -3164,20 +3136,6 @@ dependencies = [
"unicode-general-category",
]
[[package]]
name = "markup5ever"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
dependencies = [
"log",
"phf 0.8.0",
"phf_codegen 0.8.0",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever"
version = "0.11.0"
@ -3186,34 +3144,22 @@ checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
"log",
"phf 0.10.1",
"phf_codegen 0.10.0",
"phf_codegen",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
dependencies = [
"html5ever 0.25.2",
"markup5ever 0.10.1",
"tendril",
"xml5ever 0.16.2",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2"
dependencies = [
"html5ever 0.26.0",
"markup5ever 0.11.0",
"html5ever",
"markup5ever",
"tendril",
"xml5ever 0.17.0",
"xml5ever",
]
[[package]]
@ -3406,19 +3352,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nom"
version = "6.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
dependencies = [
"bitvec",
"funty",
"lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "nom"
version = "7.1.1"
@ -3571,7 +3504,7 @@ dependencies = [
"lazy_static",
"percent-encoding",
"pin-project",
"rand 0.8.5",
"rand",
"thiserror",
]
@ -3648,7 +3581,7 @@ dependencies = [
"once_cell",
"opentelemetry_api",
"percent-encoding",
"rand 0.8.5",
"rand",
"thiserror",
"tokio",
"tokio-stream",
@ -3801,15 +3734,6 @@ dependencies = [
"sha1",
]
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_shared 0.8.0",
]
[[package]]
name = "phf"
version = "0.10.1"
@ -3828,36 +3752,16 @@ dependencies = [
"phf_shared 0.11.1",
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
"phf_generator 0.8.0",
"phf_shared 0.8.0",
]
[[package]]
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator 0.10.0",
"phf_generator",
"phf_shared 0.10.0",
]
[[package]]
name = "phf_generator"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [
"phf_shared 0.8.0",
"rand 0.7.3",
]
[[package]]
name = "phf_generator"
version = "0.10.0"
@ -3865,16 +3769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand 0.8.5",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
"siphasher",
"rand",
]
[[package]]
@ -4042,7 +3937,7 @@ dependencies = [
"hmac",
"md-5",
"memchr",
"rand 0.8.5",
"rand",
"sha2",
"stringprep",
]
@ -4238,26 +4133,6 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
"rand_pcg",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -4265,18 +4140,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
"rand_chacha",
"rand_core",
]
[[package]]
@ -4286,16 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
"rand_core",
]
[[package]]
@ -4304,25 +4160,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.10",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
"getrandom",
]
[[package]]
@ -4422,6 +4260,7 @@ version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
dependencies = [
"async-compression",
"base64 0.21.2",
"bytes",
"encoding_rs",
@ -4479,7 +4318,7 @@ checksum = "1b97ad83c2fc18113346b7158d79732242002427c30f620fa817c1f32901e0a8"
dependencies = [
"anyhow",
"async-trait",
"getrandom 0.2.10",
"getrandom",
"matchit 0.7.0",
"opentelemetry 0.16.0",
"reqwest",
@ -4790,13 +4629,13 @@ dependencies = [
[[package]]
name = "select"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee061f90afcc8678bef7a78d0d121683f0ba753f740ff7005f833ec445876b7"
checksum = "6f9da09dc3f4dfdb6374cbffff7a2cffcec316874d4429899eefdc97b3b94dcd"
dependencies = [
"bit-set",
"html5ever 0.25.2",
"markup5ever_rcdom 0.1.0",
"html5ever",
"markup5ever_rcdom",
]
[[package]]
@ -5087,12 +4926,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "storage-path-generator"
version = "0.1.1"
@ -5119,7 +4952,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
dependencies = [
"phf_generator 0.10.0",
"phf_generator",
"phf_shared 0.10.0",
"proc-macro2",
"quote",
@ -5223,12 +5056,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "task-local-extensions"
version = "0.1.4"
@ -5600,7 +5427,7 @@ dependencies = [
"base32",
"constant_time_eq",
"hmac",
"rand 0.8.5",
"rand",
"sha1",
"sha2",
"url",
@ -5618,7 +5445,7 @@ dependencies = [
"indexmap 1.9.1",
"pin-project",
"pin-project-lite",
"rand 0.8.5",
"rand",
"slab",
"tokio",
"tokio-util",
@ -5976,7 +5803,7 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
dependencies = [
"getrandom 0.2.10",
"getrandom",
"serde",
]
@ -6025,12 +5852,6 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -6143,12 +5964,12 @@ dependencies = [
[[package]]
name = "webmention"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c1f9ad3af9421b7e94faef6f884d32bd60b6ea00ff05d84df74392a89c2b9f"
checksum = "8d07b90492f7b6fe35f5298fcd01c663d3c453e8c302dc86c7292c6681b8117d"
dependencies = [
"anyhow",
"nom 6.1.2",
"nom",
"reqwest",
"select",
"serde",
@ -6162,8 +5983,8 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8598785beeb5af95abe95e7bb20c7e747d1188347080d6811d5a56d2b9a5f368"
dependencies = [
"html5ever 0.26.0",
"markup5ever_rcdom 0.2.0",
"html5ever",
"markup5ever_rcdom",
"serde",
"serde_json",
]
@ -6402,24 +6223,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "xml5ever"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865"
dependencies = [
"log",
"mac",
"markup5ever 0.10.1",
"time 0.1.44",
]
[[package]]
name = "xml5ever"
version = "0.17.0"
@ -6428,7 +6231,7 @@ checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650"
dependencies = [
"log",
"mac",
"markup5ever 0.11.0",
"markup5ever",
]
[[package]]

View File

@ -89,7 +89,7 @@ tracing-error = "0.2.0"
tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = { version = "2.4.0", features = ["serde"] }
reqwest = { version = "0.11.18", features = ["json", "blocking"] }
reqwest = { version = "0.11.18", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.2"
reqwest-tracing = "0.4.5"
clokwerk = "0.4.0"

View File

@ -112,8 +112,7 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
## Installation
- [Docker](https://join-lemmy.org/docs/administration/install_docker.html)
- [Ansible](https://join-lemmy.org/docs/administration/install_ansible.html)
- [Lemmy Administration Docs](https://join-lemmy.org/docs/administration/administration.html)
## Lemmy Projects

View File

@ -1,3 +1,99 @@
# Lemmy v0.18.3 Release (2023-07-28)
## What is Lemmy?
Lemmy is a self-hosted social link aggregation and discussion platform. It is completely free and open, and not controlled by any company. This means that there is no advertising, tracking, or secret algorithms. Content is organized into communities, so it is easy to subscribe to topics that you are interested in, and ignore others. Voting is used to bring the most interesting items to the top.
## Major Changes
This version brings major optimizations to the database queries, which significantly reduces CPU usage. There is also a change to the way federation activities are stored, which reduces database size by around 80%. Special thanks to @phiresky for their work on DB optimizations.
The federation code now includes a check for dead instances which is used when sending activities. This helps to reduce the amount of outgoing POST requests, and also reduce server load.
In terms of security, Lemmy now performs HTML sanitization on all messages which are submitted through the API or received via federation. Together with the tightened content-security-policy from 0.18.2, cross-site scripting attacks are now much more difficult.
Other than that, there are numerous bug fixes and minor enhancements.
## Support development
@dessalines and @nutomic are working full-time on Lemmy to integrate community contributions, fix bugs, optimize performance and much more. This work is funded exclusively through donations.
If you like using Lemmy, and want to make sure that we will always be available to work full time building it, consider [donating to support its development](https://join-lemmy.org/donate). No one likes recurring donations, but theyve proven to be the only way that open-source software like Lemmy can stay independent and alive.
- [Liberapay](https://liberapay.com/Lemmy) (preferred option)
- [Open Collective](https://opencollective.com/lemmy)
- [Patreon](https://www.patreon.com/dessalines)
- [Cryptocurrency](https://join-lemmy.org/donate) (scroll to bottom of page)
## Upgrade instructions
Follow the upgrade instructions for [ansible](https://github.com/LemmyNet/lemmy-ansible#upgrading) or [docker](https://join-lemmy.org/docs/en/administration/install_docker.html#updating). There are no config or API changes with this release.
This upgrade takes ~5 minutes for the database migrations to complete.
If you need help with the upgrade, you can ask in our [support forum](https://lemmy.ml/c/lemmy_support) or on the [Matrix Chat](https://matrix.to/#/#lemmy-admin-support-topics:discuss.online).
## Changes
### Lemmy
- Restore markdown quotes after sanitize ([#3708](https://github.com/LemmyNet/lemmy/issues/3708)) ([#3749](https://github.com/LemmyNet/lemmy/issues/3749))
- remove performance-problematic and buggy duplicate site aggregates ([#3732](https://github.com/LemmyNet/lemmy/issues/3732))
- remove n^2 part of person triggers, improve community aggregate trigger ([#3739](https://github.com/LemmyNet/lemmy/issues/3739))
- Revert "Add controversial ranking ([#3205](https://github.com/LemmyNet/lemmy/issues/3205))"
- Omit local instance from federated instances list ([#3712](https://github.com/LemmyNet/lemmy/issues/3712))
- add trigram index to search ([#3719](https://github.com/LemmyNet/lemmy/issues/3719))
- Federation tests replication round1 - demonstrate absent replication of comment deletes ([#3657](https://github.com/LemmyNet/lemmy/issues/3657))
- Make resolve_object not require auth [#3685](https://github.com/LemmyNet/lemmy/issues/3685) ([#3716](https://github.com/LemmyNet/lemmy/issues/3716))
- Sanitize html ([#3708](https://github.com/LemmyNet/lemmy/issues/3708))
- Add controversial ranking ([#3205](https://github.com/LemmyNet/lemmy/issues/3205))
- Skip fragile API tests ([#3723](https://github.com/LemmyNet/lemmy/issues/3723))
- Enable gzip for reqwest ([#3696](https://github.com/LemmyNet/lemmy/issues/3696))
- Dont authenticate user after successful password reset [#3714](https://github.com/LemmyNet/lemmy/issues/3714) ([#3715](https://github.com/LemmyNet/lemmy/issues/3715))
- Bump version of dependency "webmention" ([#3711](https://github.com/LemmyNet/lemmy/issues/3711))
- prevent ordering by comment path without post filter ([#3717](https://github.com/LemmyNet/lemmy/issues/3717))
- Update Dockerfile to run process as non-privileged user. ([#3709](https://github.com/LemmyNet/lemmy/issues/3709))
- Dont show removed comments to unauthenticated users (release branch) ([#3689](https://github.com/LemmyNet/lemmy/issues/3689))
- Add dev profile to strip symbols and disable debug info (ref [#3610](https://github.com/LemmyNet/lemmy/issues/3610)) ([#3611](https://github.com/LemmyNet/lemmy/issues/3611))
- Dont publish releases to crates.io (fixes [#3272](https://github.com/LemmyNet/lemmy/issues/3272)) ([#3664](https://github.com/LemmyNet/lemmy/issues/3664))
- Change logic for determining comment default language (fixes [#3451](https://github.com/LemmyNet/lemmy/issues/3451)) ([#3672](https://github.com/LemmyNet/lemmy/issues/3672))
- Post remove delete federation outbound fix0 ([#3613](https://github.com/LemmyNet/lemmy/issues/3613))
- disable rustfmt feature on rosetta-build ([#3679](https://github.com/LemmyNet/lemmy/issues/3679))
- Make sure comments are sorted by hot_rank, then score. ([#3667](https://github.com/LemmyNet/lemmy/issues/3667))
- Ignore errors when fetching community mods (fixes [#3460](https://github.com/LemmyNet/lemmy/issues/3460)) ([#3674](https://github.com/LemmyNet/lemmy/issues/3674))
- Upgrade activitypub library to 0.4.6 (fixes [#3222](https://github.com/LemmyNet/lemmy/issues/3222)) ([#3675](https://github.com/LemmyNet/lemmy/issues/3675))
- Denormalize community_id into post_aggregates for a 1000x speed-up when loading posts ([#3653](https://github.com/LemmyNet/lemmy/issues/3653))
- Fixing hot_ranks and scores to append a published sort. ([#3618](https://github.com/LemmyNet/lemmy/issues/3618))
- Use local_site.default_post_listing_type as the initial default listing type for new users ([#3666](https://github.com/LemmyNet/lemmy/issues/3666))
- Don't panic when scheduled tasks can't connect to database ([#3634](https://github.com/LemmyNet/lemmy/issues/3634))
- Add http cache for webfingers ([#3317](https://github.com/LemmyNet/lemmy/issues/3317))
- Optimize hot rank updates ([#3617](https://github.com/LemmyNet/lemmy/issues/3617))
- Split activity table into sent and received parts (fixes [#3103](https://github.com/LemmyNet/lemmy/issues/3103)) ([#3583](https://github.com/LemmyNet/lemmy/issues/3583))
- work around race condition on community fetch ([#3414](https://github.com/LemmyNet/lemmy/issues/3414))
- Make `lemmy_api_common` wasm-compatible ([#3587](https://github.com/LemmyNet/lemmy/issues/3587))
- Check for dead federated instances (fixes [#2221](https://github.com/LemmyNet/lemmy/issues/2221)) ([#3427](https://github.com/LemmyNet/lemmy/issues/3427))
- Fix wrong SMTP port when TLS is being used (fixes [#3574](https://github.com/LemmyNet/lemmy/issues/3574)) ([#3607](https://github.com/LemmyNet/lemmy/issues/3607))
- Add infinite scroll user option ([#3572](https://github.com/LemmyNet/lemmy/issues/3572))
- Shrink capacity in `RateLimitStorage::remove_older_than` ([#3536](https://github.com/LemmyNet/lemmy/issues/3536))
- Fix [#3501](https://github.com/LemmyNet/lemmy/issues/3501) - Fix aggregation counts for elements removed and deleted ([#3543](https://github.com/LemmyNet/lemmy/issues/3543))
### Lemmy-UI
- Fixing comment report showing dot. ([#1989](https://github.com/LemmyNet/lemmy-ui/issues/1989))
- Make sure comment score color matches your vote. ([#1988](https://github.com/LemmyNet/lemmy-ui/issues/1988))
- Allow limited set of markdown in title rendering ([#1977](https://github.com/LemmyNet/lemmy-ui/issues/1977))
- Allow selecting from all languages in person settings (fixes [#1971](https://github.com/LemmyNet/lemmy-ui/issues/1971)) ([#1985](https://github.com/LemmyNet/lemmy-ui/issues/1985))
- Separate final comment row + add classes ([#1982](https://github.com/LemmyNet/lemmy-ui/issues/1982))
- Fix CSP in dev mode ([#1918](https://github.com/LemmyNet/lemmy-ui/issues/1918))
- Fix base.output (see [#1911](https://github.com/LemmyNet/lemmy-ui/issues/1911)) ([#1943](https://github.com/LemmyNet/lemmy-ui/issues/1943))
- Add show/hide button to password fields ([#1861](https://github.com/LemmyNet/lemmy-ui/issues/1861))
- Fix start_url and scope ([#1931](https://github.com/LemmyNet/lemmy-ui/issues/1931))
- Remove lodash.merge dependency ([#1911](https://github.com/LemmyNet/lemmy-ui/issues/1911))
- Set person_id to myId in handleLeaveModTeam ([#1929](https://github.com/LemmyNet/lemmy-ui/issues/1929))
- Remove invalid default option from language list ([#1919](https://github.com/LemmyNet/lemmy-ui/issues/1919))
- Comment border tweak ([#1820](https://github.com/LemmyNet/lemmy-ui/issues/1820))
- Add Toast Messages for Bad Logins ([#1874](https://github.com/LemmyNet/lemmy-ui/issues/1874))
# Lemmy v0.18.1 Release (2023-07-07)
## What is Lemmy?

View File

@ -13,7 +13,7 @@ popd
yarn
yarn api-test || true
killall lemmy_server
killall -s1 lemmy_server
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
psql "$LEMMY_DATABASE_URL" -c "DROP DATABASE $INSTANCE"

View File

@ -112,8 +112,27 @@ test("Update a comment", async () => {
});
test("Delete a comment", async () => {
// creating a comment on alpha (remote from home of community)
let commentRes = await createComment(alpha, postRes.post_view.post.id);
// Find the comment on beta (home of community)
let betaComment = (
await resolveComment(beta, commentRes.comment_view.comment)
).comment;
if (!betaComment) {
throw "Missing beta comment before delete";
}
// Find the comment on remote instance gamma
let gammaComment = (
await resolveComment(gamma, commentRes.comment_view.comment)
).comment;
if (!gammaComment) {
throw "Missing gamma comment (remote-home-remote replication) before delete";
}
let deleteCommentRes = await deleteComment(
alpha,
true,
@ -126,6 +145,12 @@ test("Delete a comment", async () => {
resolveComment(beta, commentRes.comment_view.comment),
).rejects.toBe("couldnt_find_object");
// Make sure that comment is undefined on gamma after delete
await expect(
resolveComment(gamma, commentRes.comment_view.comment),
).rejects.toBe("couldnt_find_object");
// Test undeleting the comment
let undeleteCommentRes = await deleteComment(
alpha,
false,
@ -141,7 +166,7 @@ test("Delete a comment", async () => {
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
});
test("Remove a comment from admin and community on the same instance", async () => {
test.skip("Remove a comment from admin and community on the same instance", async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
// Get the id for beta
@ -225,10 +250,22 @@ test("Remove a comment from admin and community on different instance", async ()
test("Unlike a comment", async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
// Lemmy automatically creates 1 like (vote) by author of comment.
// Make sure that comment is liked (voted up) on gamma, downstream peer
// This is testing replication from remote-home-remote (alpha-beta-gamma)
let gammaComment1 = (
await resolveComment(gamma, commentRes.comment_view.comment)
).comment;
expect(gammaComment1).toBeDefined();
expect(gammaComment1?.community.local).toBe(false);
expect(gammaComment1?.creator.local).toBe(false);
expect(gammaComment1?.counts.score).toBe(1);
let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment);
expect(unlike.comment_view.counts.score).toBe(0);
// Make sure that post is unliked on beta
// Make sure that comment is unliked on beta
let betaComment = (
await resolveComment(beta, commentRes.comment_view.comment)
).comment;
@ -236,6 +273,16 @@ test("Unlike a comment", async () => {
expect(betaComment?.community.local).toBe(true);
expect(betaComment?.creator.local).toBe(false);
expect(betaComment?.counts.score).toBe(0);
// Make sure that comment is unliked on gamma, downstream peer
// This is testing replication from remote-home-remote (alpha-beta-gamma)
let gammaComment = (
await resolveComment(gamma, commentRes.comment_view.comment)
).comment;
expect(gammaComment).toBeDefined();
expect(gammaComment?.community.local).toBe(false);
expect(gammaComment?.creator.local).toBe(false);
expect(gammaComment?.counts.score).toBe(0);
});
test("Federated comment like", async () => {

View File

@ -18,6 +18,9 @@ import {
createPost,
getPost,
resolvePost,
registerUser,
API,
getPosts,
} from "./shared";
beforeAll(async () => {
@ -233,3 +236,46 @@ test("Admin actions in remote community are not federated to origin", async () =
let gammaPost2 = await getPost(gamma, gammaPost.post.id);
expect(gammaPost2.post_view.creator_banned_from_community).toBe(false);
});
test("moderator view", async () => {
// register a new user with their own community on alpha and post to it
let otherUser: API = {
auth: (await registerUser(alpha)).jwt ?? "",
client: alpha.client,
};
expect(otherUser.auth).not.toBe("");
let otherCommunity = (await createCommunity(otherUser)).community_view;
expect(otherCommunity.community.name).toBeDefined();
let otherPost = (await createPost(otherUser, otherCommunity.community.id))
.post_view;
expect(otherPost.post.id).toBeDefined();
// create a community and post on alpha
let alphaCommunity = (await createCommunity(alpha)).community_view;
expect(alphaCommunity.community.name).toBeDefined();
let alphaPost = (await createPost(alpha, alphaCommunity.community.id))
.post_view;
expect(alphaPost.post.id).toBeDefined();
// other user also posts on alpha's community
let otherAlphaPost = (
await createPost(otherUser, alphaCommunity.community.id)
).post_view;
expect(otherAlphaPost.post.id).toBeDefined();
// alpha lists posts on home page, should contain all posts that were made
let posts = (await getPosts(alpha)).posts;
expect(posts).toBeDefined();
let postIds = posts.map(post => post.post.id);
expect(postIds).toContain(otherPost.post.id);
expect(postIds).toContain(alphaPost.post.id);
expect(postIds).toContain(otherAlphaPost.post.id);
// in moderator view, alpha should not see otherPost, wich was posted on a community alpha doesn't moderate
posts = (await getPosts(alpha, true)).posts;
expect(posts).toBeDefined();
postIds = posts.map(post => post.post.id);
expect(postIds).not.toContain(otherPost.post.id);
expect(postIds).toContain(alphaPost.post.id);
expect(postIds).toContain(otherAlphaPost.post.id);
});

View File

@ -36,6 +36,7 @@ import {
resolveCommunity,
} from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
let betaCommunity: CommunityView | undefined;
@ -412,7 +413,7 @@ test("Enforce site ban for federated user", async () => {
expect(alphaUserOnBeta2.person?.person.banned).toBe(false);
});
test("Enforce community ban for federated user", async () => {
test.skip("Enforce community ban for federated user", async () => {
if (!betaCommunity) {
throw "Missing beta community";
}
@ -504,3 +505,21 @@ test("Report a post", async () => {
expect(betaReport.original_post_body).toBe(alphaReport.original_post_body);
expect(betaReport.reason).toBe(alphaReport.reason);
});
test("Sanitize HTML", async () => {
let betaCommunity = (await resolveBetaCommunity(beta)).community;
if (!betaCommunity) {
throw "Missing beta community";
}
let name = randomString(5);
let body = "<script>alert('xss');</script> hello";
let form: CreatePost = {
name,
body,
auth: beta.auth,
community_id: betaCommunity.community.id,
};
let post = await beta.client.createPost(form);
expect(post.post_view.post.body).toBe(" hello");
});

View File

@ -58,6 +58,8 @@ import { CommentReportResponse } from "lemmy-js-client/dist/types/CommentReportR
import { CreateCommentReport } from "lemmy-js-client/dist/types/CreateCommentReport";
import { ListCommentReportsResponse } from "lemmy-js-client/dist/types/ListCommentReportsResponse";
import { ListCommentReports } from "lemmy-js-client/dist/types/ListCommentReports";
import { GetPostsResponse } from "lemmy-js-client/dist/types/GetPostsResponse";
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";
@ -611,6 +613,8 @@ export async function registerUser(
export async function saveUserSettingsBio(api: API): Promise<LoginResponse> {
let form: SaveUserSettings = {
show_nsfw: true,
blur_nsfw: false,
auto_expand: true,
theme: "darkly",
default_sort_type: "Active",
default_listing_type: "All",
@ -631,6 +635,8 @@ export async function saveUserSettingsFederated(
let bio = "a changed bio";
let form: SaveUserSettings = {
show_nsfw: false,
blur_nsfw: true,
auto_expand: false,
default_sort_type: "Hot",
default_listing_type: "All",
interface_language: "",
@ -753,6 +759,17 @@ export async function listCommentReports(
return api.client.listCommentReports(form);
}
export function getPosts(
api: API,
moderator_view = false,
): Promise<GetPostsResponse> {
let form: GetPosts = {
moderator_view,
auth: api.auth,
};
return api.client.getPosts(form);
}
export function delay(millis = 500) {
return new Promise(resolve => setTimeout(resolve, millis));
}

View File

@ -2,6 +2,11 @@
# yarn lockfile v1
"@aashutoshrathi/word-wrap@^1.2.3":
version "1.2.6"
resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
"@ampproject/remapping@^2.2.0":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
@ -496,6 +501,13 @@
dependencies:
"@sinclair/typebox" "^0.25.16"
"@jest/schemas@^29.6.0":
version "29.6.0"
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040"
integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==
dependencies:
"@sinclair/typebox" "^0.27.8"
"@jest/source-map@^29.4.3":
version "29.4.3"
resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20"
@ -621,6 +633,11 @@
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718"
integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==
"@sinclair/typebox@^0.27.8":
version "0.27.8"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==
"@sinonjs/commons@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
@ -1131,11 +1148,11 @@ convert-source-map@^2.0.0:
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cross-fetch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
version "3.1.8"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
dependencies:
node-fetch "2.6.7"
node-fetch "^2.6.12"
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
@ -2297,10 +2314,10 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
node-fetch@^2.6.12:
version "2.6.12"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==
dependencies:
whatwg-url "^5.0.0"
@ -2310,9 +2327,9 @@ node-int64@^0.4.0:
integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
node-releases@^2.0.8:
version "2.0.10"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
version "2.0.13"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
normalize-path@^3.0.0:
version "3.0.0"
@ -2341,16 +2358,16 @@ onetime@^5.1.2:
mimic-fn "^2.1.0"
optionator@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
version "0.9.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==
dependencies:
"@aashutoshrathi/word-wrap" "^1.2.3"
deep-is "^0.1.3"
fast-levenshtein "^2.0.6"
levn "^0.4.1"
prelude-ls "^1.2.1"
type-check "^0.4.0"
word-wrap "^1.2.3"
p-limit@^2.2.0:
version "2.3.0"
@ -2438,9 +2455,9 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1:
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pirates@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
version "4.0.6"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
pkg-dir@^4.2.0:
version "4.2.0"
@ -2467,11 +2484,11 @@ prettier@^3.0.0:
integrity sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==
pretty-format@^29.0.0, pretty-format@^29.5.0:
version "29.5.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a"
integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==
version "29.6.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.1.tgz#ec838c288850b7c4f9090b867c2d4f4edbfb0f3e"
integrity sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==
dependencies:
"@jest/schemas" "^29.4.3"
"@jest/schemas" "^29.6.0"
ansi-styles "^5.0.0"
react-is "^18.0.0"
@ -2558,18 +2575,18 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
semver@7.x, semver@^7.3.5, semver@^7.3.7:
version "7.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0"
integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==
semver@^6.0.0, semver@^6.3.0:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.5, semver@^7.3.7, semver@^7.5.3:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
semver@^6.0.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -2724,9 +2741,9 @@ tr46@~0.0.3:
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
ts-jest@^29.1.0:
version "29.1.0"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.0.tgz#4a9db4104a49b76d2b368ea775b6c9535c603891"
integrity sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==
version "29.1.1"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b"
integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==
dependencies:
bs-logger "0.x"
fast-json-stable-stringify "2.x"
@ -2734,7 +2751,7 @@ ts-jest@^29.1.0:
json5 "^2.2.3"
lodash.memoize "4.x"
make-error "1.x"
semver "7.x"
semver "^7.5.3"
yargs-parser "^21.0.1"
tslib@^1.8.1:
@ -2772,9 +2789,9 @@ type-fest@^0.21.3:
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typescript@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
update-browserslist-db@^1.0.10:
version "1.0.11"
@ -2827,11 +2844,6 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
word-wrap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"

View File

@ -20,6 +20,7 @@ lemmy_db_views = { workspace = true, features = ["full"] }
lemmy_db_views_moderator = { workspace = true, features = ["full"] }
lemmy_db_views_actor = { workspace = true, features = ["full"] }
lemmy_api_common = { workspace = true, features = ["full"] }
activitypub_federation = { workspace = true }
bcrypt = { workspace = true }
serde = { workspace = true }
actix-web = { workspace = true }

View File

@ -1,5 +1,4 @@
use crate::Perform;
use actix_web::web::Data;
use actix_web::web::{Data, Json};
use lemmy_api_common::{
comment::{CommentResponse, DistinguishComment},
context::LemmyContext,
@ -12,50 +11,47 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::CommentView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)]
impl Perform for DistinguishComment {
type Response = CommentResponse;
#[tracing::instrument(skip(context))]
pub async fn distinguish_comment(
data: Json<DistinguishComment>,
context: Data<LemmyContext>,
) -> Result<Json<CommentResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommentResponse, LemmyError> {
let data: &DistinguishComment = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
&mut context.pool(),
)
.await?;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
&mut context.pool(),
)
.await?;
// Verify that only a mod or admin can distinguish a comment
is_mod_or_admin(
&mut context.pool(),
local_user_view.person.id,
orig_comment.community.id,
)
.await?;
// Verify that only a mod or admin can distinguish a comment
is_mod_or_admin(
&mut context.pool(),
local_user_view.person.id,
orig_comment.community.id,
)
.await?;
// Update the Comment
let comment_id = data.comment_id;
let form = CommentUpdateForm::builder()
.distinguished(Some(data.distinguished))
.build();
Comment::update(&mut context.pool(), comment_id, &form)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
// Update the Comment
let comment_id = data.comment_id;
let form = CommentUpdateForm::builder()
.distinguished(Some(data.distinguished))
.build();
Comment::update(&mut context.pool(), comment_id, &form)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
Ok(CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
})
}
Ok(Json(CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
}))
}

View File

@ -1,3 +1,3 @@
mod distinguish;
mod like;
mod save;
pub mod distinguish;
pub mod like;
pub mod save;

View File

@ -1,5 +1,4 @@
use crate::Perform;
use actix_web::web::Data;
use actix_web::web::{Data, Json};
use lemmy_api_common::{
comment::{CommentResponse, SaveComment},
context::LemmyContext,
@ -12,38 +11,35 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::CommentView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)]
impl Perform for SaveComment {
type Response = CommentResponse;
#[tracing::instrument(skip(context))]
pub async fn save_comment(
data: Json<SaveComment>,
context: Data<LemmyContext>,
) -> Result<Json<CommentResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommentResponse, LemmyError> {
let data: &SaveComment = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let comment_saved_form = CommentSavedForm {
comment_id: data.comment_id,
person_id: local_user_view.person.id,
};
let comment_saved_form = CommentSavedForm {
comment_id: data.comment_id,
person_id: local_user_view.person.id,
};
if data.save {
CommentSaved::save(&mut context.pool(), &comment_saved_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
} else {
CommentSaved::unsave(&mut context.pool(), &comment_saved_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
}
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
Ok(CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
})
if data.save {
CommentSaved::save(&mut context.pool(), &comment_saved_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
} else {
CommentSaved::unsave(&mut context.pool(), &comment_saved_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
}
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
Ok(Json(CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
}))
}

View File

@ -3,7 +3,12 @@ use actix_web::web::Data;
use lemmy_api_common::{
comment::{CommentReportResponse, CreateCommentReport},
context::LemmyContext,
utils::{check_community_ban, local_user_view_from_jwt, send_new_report_email_to_admins},
utils::{
check_community_ban,
local_user_view_from_jwt,
sanitize_html,
send_new_report_email_to_admins,
},
};
use lemmy_db_schema::{
source::{
@ -29,8 +34,8 @@ impl Perform for CreateCommentReport {
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = self.reason.trim();
check_report_reason(reason, &local_site)?;
let reason = sanitize_html(self.reason.trim());
check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id;
let comment_id = data.comment_id;
@ -42,7 +47,7 @@ impl Perform for CreateCommentReport {
creator_id: person_id,
comment_id,
original_comment_text: comment_view.comment.content,
reason: reason.to_owned(),
reason,
};
let report = CommentReport::report(&mut context.pool(), &report_form)

View File

@ -1,5 +1,4 @@
use crate::Perform;
use actix_web::web::Data;
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
comment::{ListCommentReports, ListCommentReportsResponse},
context::LemmyContext,
@ -10,32 +9,26 @@ use lemmy_utils::error::LemmyError;
/// Lists comment reports for a community if an id is supplied
/// or returns all comment reports for communities a user moderates
#[async_trait::async_trait(?Send)]
impl Perform for ListCommentReports {
type Response = ListCommentReportsResponse;
#[tracing::instrument(skip(context))]
pub async fn list_comment_reports(
data: Query<ListCommentReports>,
context: Data<LemmyContext>,
) -> Result<Json<ListCommentReportsResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
#[tracing::instrument(skip(context))]
async fn perform(
&self,
context: &Data<LemmyContext>,
) -> Result<ListCommentReportsResponse, LemmyError> {
let data: &ListCommentReports = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let community_id = data.community_id;
let unresolved_only = data.unresolved_only;
let community_id = data.community_id;
let unresolved_only = data.unresolved_only;
let page = data.page;
let limit = data.limit;
let comment_reports = CommentReportQuery {
community_id,
unresolved_only,
page,
limit,
}
.list(&mut context.pool(), &local_user_view.person)
.await?;
Ok(ListCommentReportsResponse { comment_reports })
let page = data.page;
let limit = data.limit;
let comment_reports = CommentReportQuery {
community_id,
unresolved_only,
page,
limit,
}
.list(&mut context.pool(), &local_user_view.person)
.await?;
Ok(Json(ListCommentReportsResponse { comment_reports }))
}

View File

@ -1,3 +1,3 @@
mod create;
mod list;
mod resolve;
pub mod create;
pub mod list;
pub mod resolve;

View File

@ -1,5 +1,4 @@
use crate::Perform;
use actix_web::web::Data;
use actix_web::web::{Data, Json};
use lemmy_api_common::{
comment::{CommentReportResponse, ResolveCommentReport},
context::LemmyContext,
@ -10,41 +9,35 @@ use lemmy_db_views::structs::CommentReportView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
/// Resolves or unresolves a comment report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for ResolveCommentReport {
type Response = CommentReportResponse;
#[tracing::instrument(skip(context))]
pub async fn resolve_comment_report(
data: Json<ResolveCommentReport>,
context: Data<LemmyContext>,
) -> Result<Json<CommentReportResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
#[tracing::instrument(skip(context))]
async fn perform(
&self,
context: &Data<LemmyContext>,
) -> Result<CommentReportResponse, LemmyError> {
let data: &ResolveCommentReport = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let report_id = data.report_id;
let person_id = local_user_view.person.id;
let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
let report_id = data.report_id;
let person_id = local_user_view.person.id;
let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
let person_id = local_user_view.person.id;
is_mod_or_admin(&mut context.pool(), person_id, report.community.id).await?;
let person_id = local_user_view.person.id;
is_mod_or_admin(&mut context.pool(), person_id, report.community.id).await?;
if data.resolved {
CommentReport::resolve(&mut context.pool(), report_id, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
} else {
CommentReport::unresolve(&mut context.pool(), report_id, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
}
let report_id = data.report_id;
let comment_report_view =
CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
Ok(CommentReportResponse {
comment_report_view,
})
if data.resolved {
CommentReport::resolve(&mut context.pool(), report_id, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
} else {
CommentReport::unresolve(&mut context.pool(), report_id, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
}
let report_id = data.report_id;
let comment_report_view =
CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
Ok(Json(CommentReportResponse {
comment_report_view,
}))
}

View File

@ -3,7 +3,12 @@ use actix_web::web::Data;
use lemmy_api_common::{
community::{BanFromCommunity, BanFromCommunityResponse},
context::LemmyContext,
utils::{is_mod_or_admin, local_user_view_from_jwt, remove_user_data_in_community},
utils::{
is_mod_or_admin,
local_user_view_from_jwt,
remove_user_data_in_community,
sanitize_html_opt,
},
};
use lemmy_db_schema::{
source::{
@ -81,7 +86,7 @@ impl Perform for BanFromCommunity {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
reason: data.reason.clone(),
reason: sanitize_html_opt(&data.reason),
banned: Some(data.ban),
expires,
};

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
build_response::build_community_response,
community::{CommunityResponse, HideCommunity},
context::LemmyContext,
utils::{is_admin, local_user_view_from_jwt},
utils::{is_admin, local_user_view_from_jwt, sanitize_html_opt},
};
use lemmy_db_schema::{
source::{
@ -34,7 +34,7 @@ impl Perform for HideCommunity {
let mod_hide_community_form = ModHideCommunityForm {
community_id: data.community_id,
mod_person_id: local_user_view.person.id,
reason: data.reason.clone(),
reason: sanitize_html_opt(&data.reason),
hidden: Some(data.hidden),
};

View File

@ -9,15 +9,15 @@ use lemmy_utils::{
};
use std::io::Cursor;
mod comment;
mod comment_report;
mod community;
mod local_user;
mod post;
mod post_report;
mod private_message;
mod private_message_report;
mod site;
pub mod comment;
pub mod comment_report;
pub mod community;
pub mod local_user;
pub mod post;
pub mod post_report;
pub mod private_message;
pub mod private_message_report;
pub mod site;
#[async_trait::async_trait(?Send)]
pub trait Perform {
@ -60,7 +60,7 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> Result<String, LemmyEr
Ok(base64.encode(output_buffer.into_inner()))
}
/// Check size of report and remove whitespace
/// Check size of report
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> {
let slur_regex = &local_site_to_slur_regex(local_site);

View File

@ -3,7 +3,7 @@ use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
person::{BanPerson, BanPersonResponse},
utils::{is_admin, local_user_view_from_jwt, remove_user_data},
utils::{is_admin, local_user_view_from_jwt, remove_user_data, sanitize_html_opt},
};
use lemmy_db_schema::{
source::{
@ -63,7 +63,7 @@ impl Perform for BanPerson {
let form = ModBanForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
reason: data.reason.clone(),
reason: sanitize_html_opt(&data.reason),
banned: Some(data.ban),
expires,
};

View File

@ -5,15 +5,11 @@ use lemmy_api_common::{
person::{LoginResponse, PasswordChangeAfterReset},
utils::password_length_check,
};
use lemmy_db_schema::{
source::{local_user::LocalUser, password_reset_request::PasswordResetRequest},
RegistrationMode,
};
use lemmy_db_views::structs::SiteView;
use lemmy_utils::{
claims::Claims,
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
use lemmy_db_schema::source::{
local_user::LocalUser,
password_reset_request::PasswordResetRequest,
};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)]
impl Perform for PasswordChangeAfterReset {
@ -38,30 +34,12 @@ impl Perform for PasswordChangeAfterReset {
// Update the user with the new password
let password = data.password.clone();
let updated_local_user =
LocalUser::update_password(&mut context.pool(), local_user_id, &password)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
// Return the jwt if login is allowed
let site_view = SiteView::read_local(&mut context.pool()).await?;
let jwt = if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
&& !updated_local_user.accepted_application
{
None
} else {
Some(
Claims::jwt(
updated_local_user.id.0,
&context.secret().jwt_secret,
&context.settings().hostname,
)?
.into(),
)
};
LocalUser::update_password(&mut context.pool(), local_user_id, &password)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
Ok(LoginResponse {
jwt,
jwt: None,
verify_email_sent: false,
registration_created: false,
})

View File

@ -1,13 +1,13 @@
mod add_admin;
mod ban_person;
mod block;
mod change_password;
mod change_password_after_reset;
mod get_captcha;
mod list_banned;
mod login;
mod notifications;
mod report_count;
mod reset_password;
mod save_settings;
mod verify_email;
pub mod add_admin;
pub mod ban_person;
pub mod block;
pub mod change_password;
pub mod change_password_after_reset;
pub mod get_captcha;
pub mod list_banned;
pub mod login;
pub mod notifications;
pub mod report_count;
pub mod reset_password;
pub mod save_settings;
pub mod verify_email;

View File

@ -1,5 +1,4 @@
use crate::Perform;
use actix_web::web::Data;
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::{CommentReplyResponse, MarkCommentReplyAsRead},
@ -12,41 +11,35 @@ use lemmy_db_schema::{
use lemmy_db_views_actor::structs::CommentReplyView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)]
impl Perform for MarkCommentReplyAsRead {
type Response = CommentReplyResponse;
#[tracing::instrument(skip(context))]
pub async fn mark_reply_as_read(
data: Json<MarkCommentReplyAsRead>,
context: Data<LemmyContext>,
) -> Result<Json<CommentReplyResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
#[tracing::instrument(skip(context))]
async fn perform(
&self,
context: &Data<LemmyContext>,
) -> Result<CommentReplyResponse, LemmyError> {
let data = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let comment_reply_id = data.comment_reply_id;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
let comment_reply_id = data.comment_reply_id;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
if local_user_view.person.id != read_comment_reply.recipient_id {
return Err(LemmyErrorType::CouldntUpdateComment)?;
}
let comment_reply_id = read_comment_reply.id;
let read = Some(data.read);
CommentReply::update(
&mut context.pool(),
comment_reply_id,
&CommentReplyUpdateForm { read },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
let comment_reply_id = read_comment_reply.id;
let person_id = local_user_view.person.id;
let comment_reply_view =
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?;
Ok(CommentReplyResponse { comment_reply_view })
if local_user_view.person.id != read_comment_reply.recipient_id {
return Err(LemmyErrorType::CouldntUpdateComment)?;
}
let comment_reply_id = read_comment_reply.id;
let read = Some(data.read);
CommentReply::update(
&mut context.pool(),
comment_reply_id,
&CommentReplyUpdateForm { read },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
let comment_reply_id = read_comment_reply.id;
let person_id = local_user_view.person.id;
let comment_reply_view =
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?;
Ok(Json(CommentReplyResponse { comment_reply_view }))
}

View File

@ -1,6 +1,6 @@
mod list_mentions;
mod list_replies;
mod mark_all_read;
mod mark_mention_read;
mod mark_reply_read;
mod unread_count;
pub mod list_mentions;
pub mod list_replies;
pub mod mark_all_read;
pub mod mark_mention_read;
pub mod mark_reply_read;
pub mod unread_count;

View File

@ -3,7 +3,7 @@ use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
person::{LoginResponse, SaveUserSettings},
utils::{local_user_view_from_jwt, send_verification_email},
utils::{local_user_view_from_jwt, sanitize_html_opt, send_verification_email},
};
use lemmy_db_schema::{
source::{
@ -37,13 +37,16 @@ impl Perform for SaveUserSettings {
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let bio = sanitize_html_opt(&data.bio);
let display_name = sanitize_html_opt(&data.display_name);
let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let bio = diesel_option_overwrite(&data.bio);
let display_name = diesel_option_overwrite(&data.display_name);
let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
let bio = diesel_option_overwrite(bio);
let display_name = diesel_option_overwrite(display_name);
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
let email_deref = data.email.as_deref().map(str::to_lowercase);
let email = diesel_option_overwrite(&email_deref);
let email = diesel_option_overwrite(email_deref.clone());
if let Some(Some(email)) = &email {
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
@ -85,6 +88,7 @@ impl Perform for SaveUserSettings {
let person_id = local_user_view.person.id;
let default_listing_type = data.default_listing_type;
let default_sort_type = data.default_sort_type;
let theme = sanitize_html_opt(&data.theme);
let person_form = PersonUpdateForm::builder()
.display_name(display_name)
@ -124,11 +128,13 @@ impl Perform for SaveUserSettings {
.show_new_post_notifs(data.show_new_post_notifs)
.send_notifications_to_email(data.send_notifications_to_email)
.show_nsfw(data.show_nsfw)
.blur_nsfw(data.blur_nsfw)
.auto_expand(data.auto_expand)
.show_bot_accounts(data.show_bot_accounts)
.show_scores(data.show_scores)
.default_sort_type(default_sort_type)
.default_listing_type(default_listing_type)
.theme(data.theme.clone())
.theme(theme)
.interface_language(data.interface_language.clone())
.totp_2fa_secret(totp_2fa_secret)
.totp_2fa_url(totp_2fa_url)

View File

@ -3,7 +3,12 @@ use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
post::{CreatePostReport, PostReportResponse},
utils::{check_community_ban, local_user_view_from_jwt, send_new_report_email_to_admins},
utils::{
check_community_ban,
local_user_view_from_jwt,
sanitize_html,
send_new_report_email_to_admins,
},
};
use lemmy_db_schema::{
source::{
@ -26,8 +31,8 @@ impl Perform for CreatePostReport {
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = self.reason.trim();
check_report_reason(reason, &local_site)?;
let reason = sanitize_html(self.reason.trim());
check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id;
let post_id = data.post_id;
@ -41,7 +46,7 @@ impl Perform for CreatePostReport {
original_post_name: post_view.post.name,
original_post_url: post_view.post.url,
original_post_body: post_view.post.body,
reason: reason.to_owned(),
reason,
};
let report = PostReport::report(&mut context.pool(), &report_form)

View File

@ -3,7 +3,7 @@ use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse},
utils::{local_user_view_from_jwt, send_new_report_email_to_admins},
utils::{local_user_view_from_jwt, sanitize_html, send_new_report_email_to_admins},
};
use lemmy_db_schema::{
source::{
@ -25,8 +25,8 @@ impl Perform for CreatePrivateMessageReport {
let local_user_view = local_user_view_from_jwt(&self.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = self.reason.trim();
check_report_reason(reason, &local_site)?;
let reason = sanitize_html(self.reason.trim());
check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id;
let private_message_id = self.private_message_id;
@ -36,7 +36,7 @@ impl Perform for CreatePrivateMessageReport {
creator_id: person_id,
private_message_id,
original_pm_text: private_message.content,
reason: reason.to_owned(),
reason: reason.clone(),
};
let report = PrivateMessageReport::report(&mut context.pool(), &report_form)

View File

@ -3,7 +3,7 @@ use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
site::{PurgeComment, PurgeItemResponse},
utils::{is_admin, local_user_view_from_jwt},
utils::{is_admin, local_user_view_from_jwt, sanitize_html_opt},
};
use lemmy_db_schema::{
source::{
@ -38,7 +38,7 @@ impl Perform for PurgeComment {
Comment::delete(&mut context.pool(), comment_id).await?;
// Mod tables
let reason = data.reason.clone();
let reason = sanitize_html_opt(&data.reason);
let form = AdminPurgeCommentForm {
admin_person_id: local_user_view.person.id,
reason,

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
context::LemmyContext,
request::purge_image_from_pictrs,
site::{PurgeCommunity, PurgeItemResponse},
utils::{is_admin, local_user_view_from_jwt, purge_image_posts_for_community},
utils::{is_admin, local_user_view_from_jwt, purge_image_posts_for_community, sanitize_html_opt},
};
use lemmy_db_schema::{
source::{
@ -55,7 +55,7 @@ impl Perform for PurgeCommunity {
Community::delete(&mut context.pool(), community_id).await?;
// Mod tables
let reason = data.reason.clone();
let reason = sanitize_html_opt(&data.reason);
let form = AdminPurgeCommunityForm {
admin_person_id: local_user_view.person.id,
reason,

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
context::LemmyContext,
request::purge_image_from_pictrs,
site::{PurgeItemResponse, PurgePerson},
utils::{is_admin, local_user_view_from_jwt, purge_image_posts_for_person},
utils::{is_admin, local_user_view_from_jwt, purge_image_posts_for_person, sanitize_html_opt},
};
use lemmy_db_schema::{
source::{
@ -54,7 +54,7 @@ impl Perform for PurgePerson {
Person::delete(&mut context.pool(), person_id).await?;
// Mod tables
let reason = data.reason.clone();
let reason = sanitize_html_opt(&data.reason);
let form = AdminPurgePersonForm {
admin_person_id: local_user_view.person.id,
reason,

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
context::LemmyContext,
request::purge_image_from_pictrs,
site::{PurgeItemResponse, PurgePost},
utils::{is_admin, local_user_view_from_jwt},
utils::{is_admin, local_user_view_from_jwt, sanitize_html_opt},
};
use lemmy_db_schema::{
source::{
@ -50,7 +50,7 @@ impl Perform for PurgePost {
Post::delete(&mut context.pool(), post_id).await?;
// Mod tables
let reason = data.reason.clone();
let reason = sanitize_html_opt(&data.reason);
let form = AdminPurgePostForm {
admin_person_id: local_user_view.person.id,
reason,

View File

@ -30,7 +30,7 @@ impl Perform for ApproveRegistrationApplication {
is_admin(&local_user_view)?;
// Update the registration with reason, admin_id
let deny_reason = diesel_option_overwrite(&data.deny_reason);
let deny_reason = diesel_option_overwrite(data.deny_reason.clone());
let app_form = RegistrationApplicationUpdateForm {
admin_id: Some(Some(local_user_view.person.id)),
deny_reason,

View File

@ -34,6 +34,7 @@ full = [
"actix-web",
"futures",
"once_cell",
"ammonia",
]
[dependencies]
@ -66,3 +67,4 @@ once_cell = { workspace = true, optional = true }
actix-web = { workspace = true, optional = true }
# necessary for wasmt compilation
getrandom = { version = "0.2.10", features = ["js"] }
ammonia = { version = "3.3.0", optional = true }

View File

@ -23,7 +23,7 @@ use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::{error::LemmyError, utils::mention::MentionData};
pub async fn build_comment_response(
context: &Data<LemmyContext>,
context: &LemmyContext,
comment_id: CommentId,
local_user_view: Option<LocalUserView>,
form_id: Option<String>,

View File

@ -91,6 +91,8 @@ pub struct CaptchaResponse {
pub struct SaveUserSettings {
/// Show nsfw posts.
pub show_nsfw: Option<bool>,
pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
/// Show post and comment scores.
pub show_scores: Option<bool>,
/// Your user's theme.

View File

@ -75,6 +75,7 @@ pub struct GetPosts {
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub saved_only: Option<bool>,
pub moderator_view: Option<bool>,
pub auth: Option<Sensitive<String>>,
}

View File

@ -1,7 +1,7 @@
use crate::context::LemmyContext;
use activitypub_federation::config::Data;
use futures::future::BoxFuture;
use lemmy_db_schema::source::post::Post;
use lemmy_db_schema::source::{comment::Comment, post::Post};
use lemmy_utils::{error::LemmyResult, SYNCHRONOUS_FEDERATION};
use once_cell::sync::{Lazy, OnceCell};
use tokio::{
@ -22,6 +22,7 @@ pub static MATCH_OUTGOING_ACTIVITIES: OnceCell<MatchOutgoingActivitiesBoxed> = O
#[derive(Debug)]
pub enum SendActivityData {
CreatePost(Post),
CreateComment(Comment),
}
// TODO: instead of static, move this into LemmyContext. make sure that stopping the process with

View File

@ -84,7 +84,7 @@ pub struct SearchResponse {
pub struct ResolveObject {
/// Can be the full url, or a shortened version like: !fediverse@lemmy.ml
pub q: String,
pub auth: Sensitive<String>,
pub auth: Option<Sensitive<String>>,
}
#[skip_serializing_none]

View File

@ -729,31 +729,6 @@ pub async fn delete_user_account(
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::utils::{honeypot_check, password_length_check};
#[test]
#[rustfmt::skip]
fn password_length() {
assert!(password_length_check("Õ¼¾°3yË,o¸ãtÌÈú|ÇÁÙAøüÒI©·¤(T]/ð>æºWæ[C¤bªWöaÃÎñ·{=û³&§½K/c").is_ok());
assert!(password_length_check("1234567890").is_ok());
assert!(password_length_check("short").is_err());
assert!(password_length_check("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooong").is_err());
}
#[test]
fn honeypot() {
assert!(honeypot_check(&None).is_ok());
assert!(honeypot_check(&Some(String::new())).is_ok());
assert!(honeypot_check(&Some("1".to_string())).is_err());
assert!(honeypot_check(&Some("message".to_string())).is_err());
}
}
pub enum EndpointType {
Community,
Person,
@ -819,3 +794,51 @@ pub fn generate_featured_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
pub fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
Ok(Url::parse(&format!("{community_id}/moderators"))?.into())
}
/// Sanitize HTML with default options. Additionally, dont allow bypassing markdown
/// links and images
pub fn sanitize_html(data: &str) -> String {
let sanitized = ammonia::Builder::default()
.rm_tags(&["a", "img"])
.clean(data)
.to_string();
// restore markdown quotes
sanitized.replace("&gt;", ">")
}
pub fn sanitize_html_opt(data: &Option<String>) -> Option<String> {
data.as_ref().map(|d| sanitize_html(d))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::utils::{honeypot_check, password_length_check, sanitize_html};
#[test]
#[rustfmt::skip]
fn password_length() {
assert!(password_length_check("Õ¼¾°3yË,o¸ãtÌÈú|ÇÁÙAøüÒI©·¤(T]/ð>æºWæ[C¤bªWöaÃÎñ·{=û³&§½K/c").is_ok());
assert!(password_length_check("1234567890").is_ok());
assert!(password_length_check("short").is_err());
assert!(password_length_check("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooong").is_err());
}
#[test]
fn honeypot() {
assert!(honeypot_check(&None).is_ok());
assert!(honeypot_check(&Some(String::new())).is_ok());
assert!(honeypot_check(&Some("1".to_string())).is_err());
assert!(honeypot_check(&Some("message".to_string())).is_err());
}
#[test]
fn test_sanitize_html() {
let sanitized = sanitize_html("<script>alert(1);</script> hello");
assert_eq!(sanitized, " hello");
let sanitized = sanitize_html("<img src='http://example.com'> test");
assert_eq!(sanitized, " test");
}
}

View File

@ -21,6 +21,6 @@ actix-web = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
async-trait = { workspace = true }
webmention = "0.4.0"
webmention = "0.5.0"
chrono = { workspace = true }
uuid = { workspace = true }

View File

@ -1,9 +1,10 @@
use crate::PerformCrud;
use actix_web::web::Data;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
build_response::{build_comment_response, send_local_notifs},
comment::{CommentResponse, CreateComment},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_ban,
check_community_deleted_or_removed,
@ -12,6 +13,7 @@ use lemmy_api_common::{
get_post,
local_site_to_slur_regex,
local_user_view_from_jwt,
sanitize_html,
EndpointType,
},
};
@ -34,168 +36,174 @@ use lemmy_utils::{
validation::is_valid_body_field,
},
};
use std::ops::Deref;
const MAX_COMMENT_DEPTH_LIMIT: usize = 100;
#[async_trait::async_trait(?Send)]
impl PerformCrud for CreateComment {
type Response = CommentResponse;
#[tracing::instrument(skip(context))]
pub async fn create_comment(
data: Json<CreateComment>,
context: Data<LemmyContext>,
) -> Result<Json<CommentResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommentResponse, LemmyError> {
let data: &CreateComment = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
let content = remove_slurs(
&data.content.clone(),
&local_site_to_slur_regex(&local_site),
);
is_valid_body_field(&Some(content.clone()), false)?;
let content = sanitize_html(&content);
let content_slurs_removed = remove_slurs(
&data.content.clone(),
&local_site_to_slur_regex(&local_site),
);
is_valid_body_field(&Some(content_slurs_removed.clone()), false)?;
// Check for a community ban
let post_id = data.post_id;
let post = get_post(post_id, &mut context.pool()).await?;
let community_id = post.community_id;
// Check for a community ban
let post_id = data.post_id;
let post = get_post(post_id, &mut context.pool()).await?;
let community_id = post.community_id;
check_community_ban(local_user_view.person.id, community_id, &mut context.pool()).await?;
check_community_deleted_or_removed(community_id, &mut context.pool()).await?;
check_post_deleted_or_removed(&post)?;
check_community_ban(local_user_view.person.id, community_id, &mut context.pool()).await?;
check_community_deleted_or_removed(community_id, &mut context.pool()).await?;
check_post_deleted_or_removed(&post)?;
// Check if post is locked, no new comments
if post.locked {
return Err(LemmyErrorType::Locked)?;
}
// Check if post is locked, no new comments
if post.locked {
return Err(LemmyErrorType::Locked)?;
// Fetch the parent, if it exists
let parent_opt = if let Some(parent_id) = data.parent_id {
Comment::read(&mut context.pool(), parent_id).await.ok()
} else {
None
};
// If there's a parent_id, check to make sure that comment is in that post
// Strange issue where sometimes the post ID of the parent comment is incorrect
if let Some(parent) = parent_opt.as_ref() {
if parent.post_id != post_id {
return Err(LemmyErrorType::CouldntCreateComment)?;
}
check_comment_depth(parent)?;
}
// Fetch the parent, if it exists
let parent_opt = if let Some(parent_id) = data.parent_id {
Comment::read(&mut context.pool(), parent_id).await.ok()
} else {
None
};
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
data.language_id,
community_id,
)
.await?;
// If there's a parent_id, check to make sure that comment is in that post
// Strange issue where sometimes the post ID of the parent comment is incorrect
if let Some(parent) = parent_opt.as_ref() {
if parent.post_id != post_id {
return Err(LemmyErrorType::CouldntCreateComment)?;
}
check_comment_depth(parent)?;
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => Some(lid),
None => {
default_post_language(
&mut context.pool(),
community_id,
local_user_view.local_user.id,
)
.await?
}
};
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
data.language_id,
community_id,
)
.await?;
let comment_form = CommentInsertForm::builder()
.content(content.clone())
.post_id(data.post_id)
.creator_id(local_user_view.person.id)
.language_id(language_id)
.build();
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => Some(lid),
None => {
default_post_language(
&mut context.pool(),
community_id,
local_user_view.local_user.id,
)
.await?
}
};
let comment_form = CommentInsertForm::builder()
.content(content_slurs_removed.clone())
.post_id(data.post_id)
.creator_id(local_user_view.person.id)
.language_id(language_id)
.build();
// Create the comment
let parent_path = parent_opt.clone().map(|t| t.path);
let inserted_comment =
Comment::create(&mut context.pool(), &comment_form, parent_path.as_ref())
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
// Necessary to update the ap_id
let inserted_comment_id = inserted_comment.id;
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
let apub_id = generate_local_apub_endpoint(
EndpointType::Comment,
&inserted_comment_id.to_string(),
&protocol_and_hostname,
)?;
let updated_comment = Comment::update(
&mut context.pool(),
inserted_comment_id,
&CommentUpdateForm::builder().ap_id(Some(apub_id)).build(),
)
// Create the comment
let parent_path = parent_opt.clone().map(|t| t.path);
let inserted_comment = Comment::create(&mut context.pool(), &comment_form, parent_path.as_ref())
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
// Scan the comment for user mentions, add those rows
let mentions = scrape_text_for_mentions(&content_slurs_removed);
let recipient_ids = send_local_notifs(
mentions,
&updated_comment,
&local_user_view.person,
&post,
true,
context,
)
.await?;
// Necessary to update the ap_id
let inserted_comment_id = inserted_comment.id;
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
// You like your own comment by default
let like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: post.id,
person_id: local_user_view.person.id,
score: 1,
};
let apub_id = generate_local_apub_endpoint(
EndpointType::Comment,
&inserted_comment_id.to_string(),
&protocol_and_hostname,
)?;
let updated_comment = Comment::update(
&mut context.pool(),
inserted_comment_id,
&CommentUpdateForm::builder().ap_id(Some(apub_id)).build(),
)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
CommentLike::like(&mut context.pool(), &like_form)
// Scan the comment for user mentions, add those rows
let mentions = scrape_text_for_mentions(&content);
let recipient_ids = send_local_notifs(
mentions,
&updated_comment,
&local_user_view.person,
&post,
true,
&context,
)
.await?;
// You like your own comment by default
let like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: post.id,
person_id: local_user_view.person.id,
score: 1,
};
CommentLike::like(&mut context.pool(), &like_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
ActivityChannel::submit_activity(
SendActivityData::CreateComment(updated_comment.clone()),
&context,
)
.await?;
// If its a reply, mark the parent as read
if let Some(parent) = parent_opt {
let parent_id = parent.id;
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), parent_id).await;
if let Ok(reply) = comment_reply {
CommentReply::update(
&mut context.pool(),
reply.id,
&CommentReplyUpdateForm { read: Some(true) },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
// If its a reply, mark the parent as read
if let Some(parent) = parent_opt {
let parent_id = parent.id;
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), parent_id).await;
if let Ok(reply) = comment_reply {
CommentReply::update(
&mut context.pool(),
reply.id,
&CommentReplyUpdateForm { read: Some(true) },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateReplies)?;
}
// If the parent has PersonMentions mark them as read too
let person_id = local_user_view.person.id;
let person_mention =
PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(mention) = person_mention {
PersonMention::update(
&mut context.pool(),
mention.id,
&PersonMentionUpdateForm { read: Some(true) },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePersonMentions)?;
}
.with_lemmy_type(LemmyErrorType::CouldntUpdateReplies)?;
}
// If the parent has PersonMentions mark them as read too
let person_id = local_user_view.person.id;
let person_mention =
PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(mention) = person_mention {
PersonMention::update(
&mut context.pool(),
mention.id,
&PersonMentionUpdateForm { read: Some(true) },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePersonMentions)?;
}
}
Ok(Json(
build_comment_response(
context,
context.deref(),
inserted_comment.id,
Some(local_user_view),
self.form_id.clone(),
data.form_id.clone(),
recipient_ids,
)
.await
}
.await?,
))
}
pub fn check_comment_depth(comment: &Comment) -> Result<(), LemmyError> {

View File

@ -15,6 +15,7 @@ use lemmy_db_schema::{
};
use lemmy_db_views::structs::CommentView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
use std::ops::Deref;
#[async_trait::async_trait(?Send)]
impl PerformCrud for DeleteComment {
@ -68,7 +69,7 @@ impl PerformCrud for DeleteComment {
.await?;
build_comment_response(
context,
context.deref(),
updated_comment.id,
Some(local_user_view),
None,

View File

@ -1,5 +1,5 @@
mod create;
mod delete;
mod read;
mod remove;
mod update;
pub mod create;
pub mod delete;
pub mod read;
pub mod remove;
pub mod update;

View File

@ -1,5 +1,4 @@
use crate::PerformCrud;
use actix_web::web::Data;
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
build_response::build_comment_response,
comment::{CommentResponse, GetComment},
@ -8,19 +7,19 @@ use lemmy_api_common::{
};
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_utils::error::LemmyError;
use std::ops::Deref;
#[async_trait::async_trait(?Send)]
impl PerformCrud for GetComment {
type Response = CommentResponse;
#[tracing::instrument(skip(context))]
pub async fn get_comment(
data: Query<GetComment>,
context: Data<LemmyContext>,
) -> Result<Json<CommentResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
let local_site = LocalSite::read(&mut context.pool()).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError> {
let data = self;
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), context).await;
let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?;
check_private_instance(&local_user_view, &local_site)?;
build_comment_response(context, data.id, local_user_view, None, vec![]).await
}
Ok(Json(
build_comment_response(context.deref(), data.id, local_user_view, None, vec![]).await?,
))
}

View File

@ -16,6 +16,7 @@ use lemmy_db_schema::{
};
use lemmy_db_views::structs::CommentView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
use std::ops::Deref;
#[async_trait::async_trait(?Send)]
impl PerformCrud for RemoveComment {
@ -76,7 +77,7 @@ impl PerformCrud for RemoveComment {
.await?;
build_comment_response(
context,
context.deref(),
updated_comment.id,
Some(local_user_view),
None,

View File

@ -4,7 +4,12 @@ use lemmy_api_common::{
build_response::{build_comment_response, send_local_notifs},
comment::{CommentResponse, EditComment},
context::LemmyContext,
utils::{check_community_ban, local_site_to_slur_regex, local_user_view_from_jwt},
utils::{
check_community_ban,
local_site_to_slur_regex,
local_user_view_from_jwt,
sanitize_html_opt,
},
};
use lemmy_db_schema::{
source::{
@ -24,6 +29,7 @@ use lemmy_utils::{
validation::is_valid_body_field,
},
};
use std::ops::Deref;
#[async_trait::async_trait(?Send)]
impl PerformCrud for EditComment {
@ -59,16 +65,16 @@ impl PerformCrud for EditComment {
.await?;
// Update the Content
let content_slurs_removed = data
let content = data
.content
.as_ref()
.map(|c| remove_slurs(c, &local_site_to_slur_regex(&local_site)));
is_valid_body_field(&content_slurs_removed, false)?;
is_valid_body_field(&content, false)?;
let content = sanitize_html_opt(&content);
let comment_id = data.comment_id;
let form = CommentUpdateForm::builder()
.content(content_slurs_removed)
.content(content)
.language_id(data.language_id)
.updated(Some(Some(naive_now())))
.build();
@ -90,7 +96,7 @@ impl PerformCrud for EditComment {
.await?;
build_comment_response(
context,
context.deref(),
updated_comment.id,
Some(local_user_view),
self.form_id.clone(),

View File

@ -13,6 +13,8 @@ use lemmy_api_common::{
is_admin,
local_site_to_slur_regex,
local_user_view_from_jwt,
sanitize_html,
sanitize_html_opt,
EndpointType,
},
};
@ -59,10 +61,14 @@ impl PerformCrud for CreateCommunity {
let icon = diesel_option_overwrite_to_url_create(&data.icon)?;
let banner = diesel_option_overwrite_to_url_create(&data.banner)?;
let name = sanitize_html(&data.name);
let title = sanitize_html(&data.title);
let description = sanitize_html_opt(&data.description);
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.name, &slur_regex)?;
check_slurs(&data.title, &slur_regex)?;
check_slurs_opt(&data.description, &slur_regex)?;
check_slurs(&name, &slur_regex)?;
check_slurs(&title, &slur_regex)?;
check_slurs_opt(&description, &slur_regex)?;
is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
is_valid_body_field(&data.description, false)?;
@ -83,9 +89,9 @@ impl PerformCrud for CreateCommunity {
let keypair = generate_actor_keypair()?;
let community_form = CommunityInsertForm::builder()
.name(data.name.clone())
.title(data.title.clone())
.description(data.description.clone())
.name(name)
.title(title)
.description(description)
.icon(icon)
.banner(banner)
.nsfw(data.nsfw)

View File

@ -1,5 +1,4 @@
use crate::PerformCrud;
use actix_web::web::Data;
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
community::{ListCommunities, ListCommunitiesResponse},
context::LemmyContext,
@ -9,42 +8,36 @@ use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_db_views_actor::community_view::CommunityQuery;
use lemmy_utils::error::LemmyError;
#[async_trait::async_trait(?Send)]
impl PerformCrud for ListCommunities {
type Response = ListCommunitiesResponse;
#[tracing::instrument(skip(context))]
pub async fn list_communities(
data: Query<ListCommunities>,
context: Data<LemmyContext>,
) -> Result<Json<ListCommunitiesResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
let local_site = LocalSite::read(&mut context.pool()).await?;
let is_admin = local_user_view.as_ref().map(|luv| is_admin(luv).is_ok());
#[tracing::instrument(skip(context))]
async fn perform(
&self,
context: &Data<LemmyContext>,
) -> Result<ListCommunitiesResponse, LemmyError> {
let data: &ListCommunities = self;
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), context).await;
let local_site = LocalSite::read(&mut context.pool()).await?;
let is_admin = local_user_view.as_ref().map(|luv| is_admin(luv).is_ok());
check_private_instance(&local_user_view, &local_site)?;
check_private_instance(&local_user_view, &local_site)?;
let sort = data.sort;
let listing_type = data.type_;
let show_nsfw = data.show_nsfw;
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,
local_user: local_user.as_ref(),
page,
limit,
is_mod_or_admin: is_admin,
..Default::default()
}
.list(&mut context.pool())
.await?;
// Return the jwt
Ok(ListCommunitiesResponse { communities })
let sort = data.sort;
let listing_type = data.type_;
let show_nsfw = data.show_nsfw;
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,
local_user: local_user.as_ref(),
page,
limit,
is_mod_or_admin: is_admin,
..Default::default()
}
.list(&mut context.pool())
.await?;
// Return the jwt
Ok(Json(ListCommunitiesResponse { communities }))
}

View File

@ -1,5 +1,5 @@
mod create;
mod delete;
mod list;
mod remove;
mod update;
pub mod create;
pub mod delete;
pub mod list;
pub mod remove;
pub mod update;

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
build_response::build_community_response,
community::{CommunityResponse, EditCommunity},
context::LemmyContext,
utils::{local_site_to_slur_regex, local_user_view_from_jwt},
utils::{local_site_to_slur_regex, local_user_view_from_jwt, sanitize_html_opt},
};
use lemmy_db_schema::{
newtypes::PersonId,
@ -32,15 +32,18 @@ impl PerformCrud for EditCommunity {
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let description = diesel_option_overwrite(&data.description);
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs_opt(&data.title, &slur_regex)?;
check_slurs_opt(&data.description, &slur_regex)?;
is_valid_body_field(&data.description, false)?;
let title = sanitize_html_opt(&data.title);
let description = sanitize_html_opt(&data.description);
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let description = diesel_option_overwrite(description);
// Verify its a mod (only mods can edit it)
let community_id = data.community_id;
let mods: Vec<PersonId> =
@ -64,7 +67,7 @@ impl PerformCrud for EditCommunity {
}
let community_form = CommunityUpdateForm::builder()
.title(data.title.clone())
.title(title)
.description(description)
.icon(icon)
.banner(banner)

View File

@ -3,7 +3,7 @@ use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{CreateCustomEmoji, CustomEmojiResponse},
utils::{is_admin, local_user_view_from_jwt},
utils::{is_admin, local_user_view_from_jwt, sanitize_html},
};
use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiInsertForm},
@ -26,11 +26,15 @@ impl PerformCrud for CreateCustomEmoji {
// Make sure user is an admin
is_admin(&local_user_view)?;
let shortcode = sanitize_html(data.shortcode.to_lowercase().trim());
let alt_text = sanitize_html(&data.alt_text);
let category = sanitize_html(&data.category);
let emoji_form = CustomEmojiInsertForm::builder()
.local_site_id(local_site.id)
.shortcode(data.shortcode.to_lowercase().trim().to_string())
.alt_text(data.alt_text.to_string())
.category(data.category.to_string())
.shortcode(shortcode)
.alt_text(alt_text)
.category(category)
.image_url(data.clone().image_url.into())
.build();
let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?;

View File

@ -3,7 +3,7 @@ use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{CustomEmojiResponse, EditCustomEmoji},
utils::{is_admin, local_user_view_from_jwt},
utils::{is_admin, local_user_view_from_jwt, sanitize_html},
};
use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiUpdateForm},
@ -26,10 +26,13 @@ impl PerformCrud for EditCustomEmoji {
// Make sure user is an admin
is_admin(&local_user_view)?;
let alt_text = sanitize_html(&data.alt_text);
let category = sanitize_html(&data.category);
let emoji_form = CustomEmojiUpdateForm::builder()
.local_site_id(local_site.id)
.alt_text(data.alt_text.to_string())
.category(data.category.to_string())
.alt_text(alt_text)
.category(category)
.image_url(data.clone().image_url.into())
.build();
let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?;

View File

@ -2,13 +2,13 @@ use actix_web::web::Data;
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
mod comment;
mod community;
mod custom_emoji;
pub mod comment;
pub mod community;
pub mod custom_emoji;
pub mod post;
mod private_message;
mod site;
mod user;
pub mod private_message;
pub mod site;
pub mod user;
#[async_trait::async_trait(?Send)]
pub trait PerformCrud {

View File

@ -14,6 +14,8 @@ use lemmy_api_common::{
local_site_to_slur_regex,
local_user_view_from_jwt,
mark_post_as_read,
sanitize_html,
sanitize_html_opt,
EndpointType,
},
};
@ -91,6 +93,11 @@ pub async fn create_post(
.map(|u| (u.title, u.description, u.embed_video_url))
.unwrap_or_default();
let name = sanitize_html(data.name.trim());
let body = sanitize_html_opt(&data.body);
let embed_title = sanitize_html_opt(&embed_title);
let embed_description = sanitize_html_opt(&embed_description);
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
CommunityLanguage::is_allowed_community_language(
@ -114,9 +121,9 @@ pub async fn create_post(
};
let post_form = PostInsertForm::builder()
.name(data.name.trim().to_owned())
.name(name)
.url(url)
.body(data.body.clone())
.body(body)
.community_id(data.community_id)
.creator_id(local_user_view.person.id)
.nsfw(data.nsfw)

View File

@ -1,5 +1,5 @@
pub mod create;
mod delete;
mod read;
mod remove;
mod update;
pub mod delete;
pub mod read;
pub mod remove;
pub mod update;

View File

@ -1,5 +1,4 @@
use crate::PerformCrud;
use actix_web::web::Data;
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{GetPost, GetPostResponse},
@ -19,107 +18,103 @@ use lemmy_db_views::{post_view::PostQuery, structs::PostView};
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)]
impl PerformCrud for GetPost {
type Response = GetPostResponse;
#[tracing::instrument(skip(context))]
pub async fn get_post(
data: Query<GetPost>,
context: Data<LemmyContext>,
) -> Result<Json<GetPostResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
let local_site = LocalSite::read(&mut context.pool()).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<GetPostResponse, LemmyError> {
let data: &GetPost = self;
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), context).await;
let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?;
check_private_instance(&local_user_view, &local_site)?;
let person_id = local_user_view.as_ref().map(|u| u.person.id);
let person_id = local_user_view.as_ref().map(|u| u.person.id);
// I'd prefer fetching the post_view by a comment join, but it adds a lot of boilerplate
let post_id = if let Some(id) = data.id {
id
} else if let Some(comment_id) = data.comment_id {
Comment::read(&mut context.pool(), comment_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?
.post_id
} else {
Err(LemmyErrorType::CouldntFindPost)?
};
// I'd prefer fetching the post_view by a comment join, but it adds a lot of boilerplate
let post_id = if let Some(id) = data.id {
id
} else if let Some(comment_id) = data.comment_id {
Comment::read(&mut context.pool(), comment_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?
.post_id
} else {
Err(LemmyErrorType::CouldntFindPost)?
};
// Check to see if the person is a mod or admin, to show deleted / removed
let community_id = Post::read(&mut context.pool(), post_id).await?.community_id;
let is_mod_or_admin = is_mod_or_admin_opt(
&mut context.pool(),
local_user_view.as_ref(),
Some(community_id),
)
.await
.is_ok();
// Check to see if the person is a mod or admin, to show deleted / removed
let community_id = Post::read(&mut context.pool(), post_id).await?.community_id;
let is_mod_or_admin = is_mod_or_admin_opt(
&mut context.pool(),
local_user_view.as_ref(),
Some(community_id),
)
.await
.is_ok();
let post_view = PostView::read(
&mut context.pool(),
post_id,
person_id,
Some(is_mod_or_admin),
)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
let post_view = PostView::read(
&mut context.pool(),
post_id,
person_id,
Some(is_mod_or_admin),
)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
// Mark the post as read
let post_id = post_view.post.id;
if let Some(person_id) = person_id {
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
}
// Necessary for the sidebar subscribed
let community_view = CommunityView::read(
&mut context.pool(),
community_id,
person_id,
Some(is_mod_or_admin),
)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
// Insert into PersonPostAggregates
// to update the read_comments count
if let Some(person_id) = person_id {
let read_comments = post_view.counts.comments;
let person_post_agg_form = PersonPostAggregatesForm {
person_id,
post_id,
read_comments,
..PersonPostAggregatesForm::default()
};
PersonPostAggregates::upsert(&mut context.pool(), &person_post_agg_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
}
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 {
url_search: Some(url.inner().as_str().into()),
..Default::default()
}
.list(&mut context.pool())
.await?;
// Don't return this post as one of the cross_posts
x_posts.retain(|x| x.post.id != post_id);
x_posts
} else {
Vec::new()
};
// Return the jwt
Ok(GetPostResponse {
post_view,
community_view,
moderators,
cross_posts,
})
// Mark the post as read
let post_id = post_view.post.id;
if let Some(person_id) = person_id {
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
}
// Necessary for the sidebar subscribed
let community_view = CommunityView::read(
&mut context.pool(),
community_id,
person_id,
Some(is_mod_or_admin),
)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
// Insert into PersonPostAggregates
// to update the read_comments count
if let Some(person_id) = person_id {
let read_comments = post_view.counts.comments;
let person_post_agg_form = PersonPostAggregatesForm {
person_id,
post_id,
read_comments,
..PersonPostAggregatesForm::default()
};
PersonPostAggregates::upsert(&mut context.pool(), &person_post_agg_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
}
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 {
url_search: Some(url.inner().as_str().into()),
..Default::default()
}
.list(&mut context.pool())
.await?;
// Don't return this post as one of the cross_posts
x_posts.retain(|x| x.post.id != post_id);
x_posts
} else {
Vec::new()
};
// Return the jwt
Ok(Json(GetPostResponse {
post_view,
community_view,
moderators,
cross_posts,
}))
}

View File

@ -5,7 +5,12 @@ use lemmy_api_common::{
context::LemmyContext,
post::{EditPost, PostResponse},
request::fetch_site_data,
utils::{check_community_ban, local_site_to_slur_regex, local_user_view_from_jwt},
utils::{
check_community_ban,
local_site_to_slur_regex,
local_user_view_from_jwt,
sanitize_html_opt,
},
};
use lemmy_db_schema::{
source::{
@ -39,7 +44,6 @@ impl PerformCrud for EditPost {
// TODO No good way to handle a clear.
// Issue link: https://github.com/LemmyNet/lemmy/issues/2287
let url = Some(data_url.map(clean_url_params).map(Into::into));
let body = diesel_option_overwrite(&data.body);
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs_opt(&data.name, &slur_regex)?;
@ -75,6 +79,12 @@ impl PerformCrud for EditPost {
.map(|u| (Some(u.title), Some(u.description), Some(u.embed_video_url)))
.unwrap_or_default();
let name = sanitize_html_opt(&data.name);
let body = sanitize_html_opt(&data.body);
let body = diesel_option_overwrite(body);
let embed_title = embed_title.map(|e| sanitize_html_opt(&e));
let embed_description = embed_description.map(|e| sanitize_html_opt(&e));
let language_id = self.language_id;
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
@ -84,7 +94,7 @@ impl PerformCrud for EditPost {
.await?;
let post_form = PostUpdateForm::builder()
.name(data.name.clone())
.name(name)
.url(url)
.body(body)
.nsfw(data.nsfw)

View File

@ -9,6 +9,7 @@ use lemmy_api_common::{
get_interface_language,
local_site_to_slur_regex,
local_user_view_from_jwt,
sanitize_html,
send_email_to_user,
EndpointType,
},
@ -39,11 +40,9 @@ impl PerformCrud for CreatePrivateMessage {
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
let content_slurs_removed = remove_slurs(
&data.content.clone(),
&local_site_to_slur_regex(&local_site),
);
is_valid_body_field(&Some(content_slurs_removed.clone()), false)?;
let content = sanitize_html(&data.content);
let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
is_valid_body_field(&Some(content.clone()), false)?;
check_person_block(
local_user_view.person.id,
@ -53,7 +52,7 @@ impl PerformCrud for CreatePrivateMessage {
.await?;
let private_message_form = PrivateMessageInsertForm::builder()
.content(content_slurs_removed.clone())
.content(content.clone())
.creator_id(local_user_view.person.id)
.recipient_id(data.recipient_id)
.build();
@ -92,7 +91,7 @@ impl PerformCrud for CreatePrivateMessage {
send_email_to_user(
&local_recipient,
&lang.notification_private_message_subject(sender_name),
&lang.notification_private_message_body(inbox_link, &content_slurs_removed, sender_name),
&lang.notification_private_message_body(inbox_link, &content, sender_name),
context.settings(),
)
.await;

View File

@ -1,4 +1,4 @@
mod create;
mod delete;
mod read;
mod update;
pub mod create;
pub mod delete;
pub mod read;
pub mod update;

View File

@ -1,5 +1,4 @@
use crate::PerformCrud;
use actix_web::web::Data;
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
private_message::{GetPrivateMessages, PrivateMessagesResponse},
@ -8,40 +7,34 @@ use lemmy_api_common::{
use lemmy_db_views::private_message_view::PrivateMessageQuery;
use lemmy_utils::error::LemmyError;
#[async_trait::async_trait(?Send)]
impl PerformCrud for GetPrivateMessages {
type Response = PrivateMessagesResponse;
#[tracing::instrument(skip(context))]
pub async fn get_private_message(
data: Query<GetPrivateMessages>,
context: Data<LemmyContext>,
) -> Result<Json<PrivateMessagesResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(data.auth.as_ref(), &context).await?;
let person_id = local_user_view.person.id;
#[tracing::instrument(skip(self, context))]
async fn perform(
&self,
context: &Data<LemmyContext>,
) -> Result<PrivateMessagesResponse, LemmyError> {
let data: &GetPrivateMessages = self;
let local_user_view = local_user_view_from_jwt(data.auth.as_ref(), context).await?;
let person_id = local_user_view.person.id;
let page = data.page;
let limit = data.limit;
let unread_only = data.unread_only;
let mut messages = PrivateMessageQuery {
page,
limit,
unread_only,
}
.list(&mut context.pool(), person_id)
.await?;
// Messages sent by ourselves should be marked as read. The `read` column in database is only
// for the recipient, and shouldnt be exposed to sender.
messages.iter_mut().for_each(|pmv| {
if pmv.creator.id == person_id {
pmv.private_message.read = true
}
});
Ok(PrivateMessagesResponse {
private_messages: messages,
})
let page = data.page;
let limit = data.limit;
let unread_only = data.unread_only;
let mut messages = PrivateMessageQuery {
page,
limit,
unread_only,
}
.list(&mut context.pool(), person_id)
.await?;
// Messages sent by ourselves should be marked as read. The `read` column in database is only
// for the recipient, and shouldnt be exposed to sender.
messages.iter_mut().for_each(|pmv| {
if pmv.creator.id == person_id {
pmv.private_message.read = true
}
});
Ok(Json(PrivateMessagesResponse {
private_messages: messages,
}))
}

View File

@ -3,7 +3,7 @@ use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
private_message::{EditPrivateMessage, PrivateMessageResponse},
utils::{local_site_to_slur_regex, local_user_view_from_jwt},
utils::{local_site_to_slur_regex, local_user_view_from_jwt, sanitize_html},
};
use lemmy_db_schema::{
source::{
@ -41,15 +41,16 @@ impl PerformCrud for EditPrivateMessage {
}
// Doing the update
let content_slurs_removed = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site));
is_valid_body_field(&Some(content_slurs_removed.clone()), false)?;
let content = sanitize_html(&data.content);
let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
is_valid_body_field(&Some(content.clone()), false)?;
let private_message_id = data.private_message_id;
PrivateMessage::update(
&mut context.pool(),
private_message_id,
&PrivateMessageUpdateForm::builder()
.content(Some(content_slurs_removed))
.content(Some(content))
.updated(Some(Some(naive_now())))
.build(),
)

View File

@ -1,9 +1,6 @@
use crate::{
site::{application_question_check, site_default_post_listing_type_check},
PerformCrud,
};
use crate::site::{application_question_check, site_default_post_listing_type_check};
use activitypub_federation::http_signatures::generate_actor_keypair;
use actix_web::web::Data;
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
site::{CreateSite, SiteResponse},
@ -12,6 +9,8 @@ use lemmy_api_common::{
is_admin,
local_site_rate_limit_to_rate_limit_config,
local_user_view_from_jwt,
sanitize_html,
sanitize_html_opt,
},
};
use lemmy_db_schema::{
@ -41,100 +40,105 @@ use lemmy_utils::{
};
use url::Url;
#[async_trait::async_trait(?Send)]
impl PerformCrud for CreateSite {
type Response = SiteResponse;
#[tracing::instrument(skip(context))]
pub async fn create_site(
data: Json<CreateSite>,
context: Data<LemmyContext>,
) -> Result<Json<SiteResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> {
let data: &CreateSite = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
// Make sure user is an admin; other types of users should not create site data...
is_admin(&local_user_view)?;
// Make sure user is an admin; other types of users should not create site data...
is_admin(&local_user_view)?;
validate_create_payload(&local_site, &data)?;
validate_create_payload(&local_site, data)?;
let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
let keypair = generate_actor_keypair()?;
let name = sanitize_html(&data.name);
let sidebar = sanitize_html_opt(&data.sidebar);
let description = sanitize_html_opt(&data.description);
let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
let keypair = generate_actor_keypair()?;
let site_form = SiteUpdateForm::builder()
.name(Some(data.name.clone()))
.sidebar(diesel_option_overwrite(&data.sidebar))
.description(diesel_option_overwrite(&data.description))
.icon(diesel_option_overwrite_to_url(&data.icon)?)
.banner(diesel_option_overwrite_to_url(&data.banner)?)
.actor_id(Some(actor_id))
.last_refreshed_at(Some(naive_now()))
.inbox_url(inbox_url)
.private_key(Some(Some(keypair.private_key)))
.public_key(Some(keypair.public_key))
.build();
let site_form = SiteUpdateForm::builder()
.name(Some(name))
.sidebar(diesel_option_overwrite(sidebar))
.description(diesel_option_overwrite(description))
.icon(diesel_option_overwrite_to_url(&data.icon)?)
.banner(diesel_option_overwrite_to_url(&data.banner)?)
.actor_id(Some(actor_id))
.last_refreshed_at(Some(naive_now()))
.inbox_url(inbox_url)
.private_key(Some(Some(keypair.private_key)))
.public_key(Some(keypair.public_key))
.build();
let site_id = local_site.site_id;
let site_id = local_site.site_id;
Site::update(&mut context.pool(), site_id, &site_form).await?;
Site::update(&mut context.pool(), site_id, &site_form).await?;
let local_site_form = LocalSiteUpdateForm::builder()
// Set the site setup to true
.site_setup(Some(true))
.enable_downvotes(data.enable_downvotes)
.registration_mode(data.registration_mode)
.enable_nsfw(data.enable_nsfw)
.community_creation_admin_only(data.community_creation_admin_only)
.require_email_verification(data.require_email_verification)
.application_question(diesel_option_overwrite(&data.application_question))
.private_instance(data.private_instance)
.default_theme(data.default_theme.clone())
.default_post_listing_type(data.default_post_listing_type)
.legal_information(diesel_option_overwrite(&data.legal_information))
.application_email_admins(data.application_email_admins)
.hide_modlog_mod_names(data.hide_modlog_mod_names)
.updated(Some(Some(naive_now())))
.slur_filter_regex(diesel_option_overwrite(&data.slur_filter_regex))
.actor_name_max_length(data.actor_name_max_length)
.federation_enabled(data.federation_enabled)
.captcha_enabled(data.captcha_enabled)
.captcha_difficulty(data.captcha_difficulty.clone())
.build();
let application_question = sanitize_html_opt(&data.application_question);
let default_theme = sanitize_html_opt(&data.default_theme);
let legal_information = sanitize_html_opt(&data.legal_information);
LocalSite::update(&mut context.pool(), &local_site_form).await?;
let local_site_form = LocalSiteUpdateForm::builder()
// Set the site setup to true
.site_setup(Some(true))
.enable_downvotes(data.enable_downvotes)
.registration_mode(data.registration_mode)
.enable_nsfw(data.enable_nsfw)
.community_creation_admin_only(data.community_creation_admin_only)
.require_email_verification(data.require_email_verification)
.application_question(diesel_option_overwrite(application_question))
.private_instance(data.private_instance)
.default_theme(default_theme)
.default_post_listing_type(data.default_post_listing_type)
.legal_information(diesel_option_overwrite(legal_information))
.application_email_admins(data.application_email_admins)
.hide_modlog_mod_names(data.hide_modlog_mod_names)
.updated(Some(Some(naive_now())))
.slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
.actor_name_max_length(data.actor_name_max_length)
.federation_enabled(data.federation_enabled)
.captcha_enabled(data.captcha_enabled)
.captcha_difficulty(data.captcha_difficulty.clone())
.build();
let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
.message(data.rate_limit_message)
.message_per_second(data.rate_limit_message_per_second)
.post(data.rate_limit_post)
.post_per_second(data.rate_limit_post_per_second)
.register(data.rate_limit_register)
.register_per_second(data.rate_limit_register_per_second)
.image(data.rate_limit_image)
.image_per_second(data.rate_limit_image_per_second)
.comment(data.rate_limit_comment)
.comment_per_second(data.rate_limit_comment_per_second)
.search(data.rate_limit_search)
.search_per_second(data.rate_limit_search_per_second)
.build();
LocalSite::update(&mut context.pool(), &local_site_form).await?;
LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
.message(data.rate_limit_message)
.message_per_second(data.rate_limit_message_per_second)
.post(data.rate_limit_post)
.post_per_second(data.rate_limit_post_per_second)
.register(data.rate_limit_register)
.register_per_second(data.rate_limit_register_per_second)
.image(data.rate_limit_image)
.image_per_second(data.rate_limit_image_per_second)
.comment(data.rate_limit_comment)
.comment_per_second(data.rate_limit_comment_per_second)
.search(data.rate_limit_search)
.search_per_second(data.rate_limit_search_per_second)
.build();
let site_view = SiteView::read_local(&mut context.pool()).await?;
LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
context
.settings_updated_channel()
.send(rate_limit_config)
.await?;
let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
Ok(SiteResponse {
site_view,
taglines,
})
}
let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
context
.settings_updated_channel()
.send(rate_limit_config)
.await?;
Ok(Json(SiteResponse {
site_view,
taglines,
}))
}
fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> {

View File

@ -1,9 +1,9 @@
use lemmy_db_schema::{ListingType, RegistrationMode};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
mod create;
mod read;
mod update;
pub mod create;
pub mod read;
pub mod update;
/// Checks whether the default post listing type is valid for a site.
pub fn site_default_post_listing_type_check(

View File

@ -1,5 +1,4 @@
use crate::PerformCrud;
use actix_web::web::Data;
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
sensitive::Sensitive,
@ -28,76 +27,72 @@ use lemmy_utils::{
version,
};
#[async_trait::async_trait(?Send)]
impl PerformCrud for GetSite {
type Response = GetSiteResponse;
#[tracing::instrument(skip(context))]
pub async fn get_site(
data: Query<GetSite>,
context: Data<LemmyContext>,
) -> Result<Json<GetSiteResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<GetSiteResponse, LemmyError> {
let data: &GetSite = self;
let admins = PersonView::admins(&mut context.pool()).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
// Build the local user
let my_user = if let Some(local_user_view) =
local_user_settings_view_from_jwt_opt(data.auth.as_ref(), &context).await
{
let person_id = local_user_view.person.id;
let local_user_id = local_user_view.local_user.id;
let admins = PersonView::admins(&mut context.pool()).await?;
let follows = CommunityFollowerView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
// Build the local user
let my_user = if let Some(local_user_view) =
local_user_settings_view_from_jwt_opt(data.auth.as_ref(), context).await
{
let person_id = local_user_view.person.id;
let local_user_id = local_user_view.local_user.id;
let person_id = local_user_view.person.id;
let community_blocks = CommunityBlockView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let follows = CommunityFollowerView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let person_id = local_user_view.person.id;
let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let person_id = local_user_view.person.id;
let community_blocks = CommunityBlockView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let moderates = CommunityModeratorView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let person_id = local_user_view.person.id;
let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let discussion_languages = LocalUserLanguage::read(&mut context.pool(), local_user_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let moderates = CommunityModeratorView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let discussion_languages = LocalUserLanguage::read(&mut context.pool(), local_user_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
Some(MyUserInfo {
local_user_view,
follows,
moderates,
community_blocks,
person_blocks,
discussion_languages,
})
} else {
None
};
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
Ok(GetSiteResponse {
site_view,
admins,
version: version::VERSION.to_string(),
my_user,
all_languages,
Some(MyUserInfo {
local_user_view,
follows,
moderates,
community_blocks,
person_blocks,
discussion_languages,
taglines,
custom_emojis,
})
}
} else {
None
};
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
Ok(Json(GetSiteResponse {
site_view,
admins,
version: version::VERSION.to_string(),
my_user,
all_languages,
discussion_languages,
taglines,
custom_emojis,
}))
}
#[tracing::instrument(skip_all)]

View File

@ -1,12 +1,14 @@
use crate::{
site::{application_question_check, site_default_post_listing_type_check},
PerformCrud,
};
use actix_web::web::Data;
use crate::site::{application_question_check, site_default_post_listing_type_check};
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
site::{EditSite, SiteResponse},
utils::{is_admin, local_site_rate_limit_to_rate_limit_config, local_user_view_from_jwt},
utils::{
is_admin,
local_site_rate_limit_to_rate_limit_config,
local_user_view_from_jwt,
sanitize_html_opt,
},
};
use lemmy_db_schema::{
source::{
@ -38,139 +40,142 @@ use lemmy_utils::{
},
};
#[async_trait::async_trait(?Send)]
impl PerformCrud for EditSite {
type Response = SiteResponse;
#[tracing::instrument(skip(context))]
pub async fn update_site(
data: Json<EditSite>,
context: Data<LemmyContext>,
) -> Result<Json<SiteResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_site = site_view.local_site;
let site = site_view.site;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> {
let data: &EditSite = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_site = site_view.local_site;
let site = site_view.site;
// Make sure user is an admin; other types of users should not update site data...
is_admin(&local_user_view)?;
// Make sure user is an admin; other types of users should not update site data...
is_admin(&local_user_view)?;
validate_update_payload(&local_site, &data)?;
validate_update_payload(&local_site, data)?;
if let Some(discussion_languages) = data.discussion_languages.clone() {
SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
}
let site_form = SiteUpdateForm::builder()
.name(data.name.clone())
.sidebar(diesel_option_overwrite(&data.sidebar))
.description(diesel_option_overwrite(&data.description))
.icon(diesel_option_overwrite_to_url(&data.icon)?)
.banner(diesel_option_overwrite_to_url(&data.banner)?)
.updated(Some(Some(naive_now())))
.build();
Site::update(&mut context.pool(), site.id, &site_form)
.await
// Ignore errors for all these, so as to not throw errors if no update occurs
// Diesel will throw an error for empty update forms
.ok();
let local_site_form = LocalSiteUpdateForm::builder()
.enable_downvotes(data.enable_downvotes)
.registration_mode(data.registration_mode)
.enable_nsfw(data.enable_nsfw)
.community_creation_admin_only(data.community_creation_admin_only)
.require_email_verification(data.require_email_verification)
.application_question(diesel_option_overwrite(&data.application_question))
.private_instance(data.private_instance)
.default_theme(data.default_theme.clone())
.default_post_listing_type(data.default_post_listing_type)
.legal_information(diesel_option_overwrite(&data.legal_information))
.application_email_admins(data.application_email_admins)
.hide_modlog_mod_names(data.hide_modlog_mod_names)
.updated(Some(Some(naive_now())))
.slur_filter_regex(diesel_option_overwrite(&data.slur_filter_regex))
.actor_name_max_length(data.actor_name_max_length)
.federation_enabled(data.federation_enabled)
.captcha_enabled(data.captcha_enabled)
.captcha_difficulty(data.captcha_difficulty.clone())
.reports_email_admins(data.reports_email_admins)
.build();
let update_local_site = LocalSite::update(&mut context.pool(), &local_site_form)
.await
.ok();
let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
.message(data.rate_limit_message)
.message_per_second(data.rate_limit_message_per_second)
.post(data.rate_limit_post)
.post_per_second(data.rate_limit_post_per_second)
.register(data.rate_limit_register)
.register_per_second(data.rate_limit_register_per_second)
.image(data.rate_limit_image)
.image_per_second(data.rate_limit_image_per_second)
.comment(data.rate_limit_comment)
.comment_per_second(data.rate_limit_comment_per_second)
.search(data.rate_limit_search)
.search_per_second(data.rate_limit_search_per_second)
.build();
LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form)
.await
.ok();
// Replace the blocked and allowed instances
let allowed = data.allowed_instances.clone();
FederationAllowList::replace(&mut context.pool(), allowed).await?;
let blocked = data.blocked_instances.clone();
FederationBlockList::replace(&mut context.pool(), blocked).await?;
// TODO can't think of a better way to do this.
// If the server suddenly requires email verification, or required applications, no old users
// will be able to log in. It really only wants this to be a requirement for NEW signups.
// So if it was set from false, to true, you need to update all current users columns to be verified.
let old_require_application =
local_site.registration_mode == RegistrationMode::RequireApplication;
let new_require_application = update_local_site
.as_ref()
.map(|ols| ols.registration_mode == RegistrationMode::RequireApplication)
.unwrap_or(false);
if !old_require_application && new_require_application {
LocalUser::set_all_users_registration_applications_accepted(&mut context.pool())
.await
.with_lemmy_type(LemmyErrorType::CouldntSetAllRegistrationsAccepted)?;
}
let new_require_email_verification = update_local_site
.as_ref()
.map(|ols| ols.require_email_verification)
.unwrap_or(false);
if !local_site.require_email_verification && new_require_email_verification {
LocalUser::set_all_users_email_verified(&mut context.pool())
.await
.with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?;
}
let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
context
.settings_updated_channel()
.send(rate_limit_config)
.await?;
let res = SiteResponse {
site_view,
taglines,
};
Ok(res)
if let Some(discussion_languages) = data.discussion_languages.clone() {
SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
}
let name = sanitize_html_opt(&data.name);
let sidebar = sanitize_html_opt(&data.sidebar);
let description = sanitize_html_opt(&data.description);
let site_form = SiteUpdateForm::builder()
.name(name)
.sidebar(diesel_option_overwrite(sidebar))
.description(diesel_option_overwrite(description))
.icon(diesel_option_overwrite_to_url(&data.icon)?)
.banner(diesel_option_overwrite_to_url(&data.banner)?)
.updated(Some(Some(naive_now())))
.build();
Site::update(&mut context.pool(), site.id, &site_form)
.await
// Ignore errors for all these, so as to not throw errors if no update occurs
// Diesel will throw an error for empty update forms
.ok();
let application_question = sanitize_html_opt(&data.application_question);
let default_theme = sanitize_html_opt(&data.default_theme);
let legal_information = sanitize_html_opt(&data.legal_information);
let local_site_form = LocalSiteUpdateForm::builder()
.enable_downvotes(data.enable_downvotes)
.registration_mode(data.registration_mode)
.enable_nsfw(data.enable_nsfw)
.community_creation_admin_only(data.community_creation_admin_only)
.require_email_verification(data.require_email_verification)
.application_question(diesel_option_overwrite(application_question))
.private_instance(data.private_instance)
.default_theme(default_theme)
.default_post_listing_type(data.default_post_listing_type)
.legal_information(diesel_option_overwrite(legal_information))
.application_email_admins(data.application_email_admins)
.hide_modlog_mod_names(data.hide_modlog_mod_names)
.updated(Some(Some(naive_now())))
.slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
.actor_name_max_length(data.actor_name_max_length)
.federation_enabled(data.federation_enabled)
.captcha_enabled(data.captcha_enabled)
.captcha_difficulty(data.captcha_difficulty.clone())
.reports_email_admins(data.reports_email_admins)
.build();
let update_local_site = LocalSite::update(&mut context.pool(), &local_site_form)
.await
.ok();
let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
.message(data.rate_limit_message)
.message_per_second(data.rate_limit_message_per_second)
.post(data.rate_limit_post)
.post_per_second(data.rate_limit_post_per_second)
.register(data.rate_limit_register)
.register_per_second(data.rate_limit_register_per_second)
.image(data.rate_limit_image)
.image_per_second(data.rate_limit_image_per_second)
.comment(data.rate_limit_comment)
.comment_per_second(data.rate_limit_comment_per_second)
.search(data.rate_limit_search)
.search_per_second(data.rate_limit_search_per_second)
.build();
LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form)
.await
.ok();
// Replace the blocked and allowed instances
let allowed = data.allowed_instances.clone();
FederationAllowList::replace(&mut context.pool(), allowed).await?;
let blocked = data.blocked_instances.clone();
FederationBlockList::replace(&mut context.pool(), blocked).await?;
// TODO can't think of a better way to do this.
// If the server suddenly requires email verification, or required applications, no old users
// will be able to log in. It really only wants this to be a requirement for NEW signups.
// So if it was set from false, to true, you need to update all current users columns to be verified.
let old_require_application =
local_site.registration_mode == RegistrationMode::RequireApplication;
let new_require_application = update_local_site
.as_ref()
.map(|ols| ols.registration_mode == RegistrationMode::RequireApplication)
.unwrap_or(false);
if !old_require_application && new_require_application {
LocalUser::set_all_users_registration_applications_accepted(&mut context.pool())
.await
.with_lemmy_type(LemmyErrorType::CouldntSetAllRegistrationsAccepted)?;
}
let new_require_email_verification = update_local_site
.as_ref()
.map(|ols| ols.require_email_verification)
.unwrap_or(false);
if !local_site.require_email_verification && new_require_email_verification {
LocalUser::set_all_users_email_verified(&mut context.pool())
.await
.with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?;
}
let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
context
.settings_updated_channel()
.send(rate_limit_config)
.await?;
Ok(Json(SiteResponse {
site_view,
taglines,
}))
}
fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> LemmyResult<()> {

View File

@ -11,6 +11,7 @@ use lemmy_api_common::{
honeypot_check,
local_site_to_slur_regex,
password_length_check,
sanitize_html,
send_new_applicant_email_to_admins,
send_verification_email,
EndpointType,
@ -92,6 +93,7 @@ impl PerformCrud for Register {
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.username, &slur_regex)?;
check_slurs_opt(&data.answer, &slur_regex)?;
let username = sanitize_html(&data.username);
let actor_keypair = generate_actor_keypair()?;
is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?;
@ -111,7 +113,7 @@ impl PerformCrud for Register {
// Register the new person
let person_form = PersonInsertForm::builder()
.name(data.username.clone())
.name(username)
.actor_id(Some(actor_id.clone()))
.private_key(Some(actor_keypair.private_key))
.public_key(actor_keypair.public_key)

View File

@ -23,7 +23,7 @@ use anyhow::anyhow;
use chrono::NaiveDateTime;
use lemmy_api_common::{
context::LemmyContext,
utils::{remove_user_data, remove_user_data_in_community},
utils::{remove_user_data, remove_user_data_in_community, sanitize_html_opt},
};
use lemmy_db_schema::{
source::{
@ -179,7 +179,7 @@ impl ActivityHandler for BlockUser {
let form = ModBanForm {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
reason: self.summary,
reason: sanitize_html_opt(&self.summary),
banned: Some(true),
expires,
};
@ -213,7 +213,7 @@ impl ActivityHandler for BlockUser {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
community_id: community.id,
reason: self.summary,
reason: sanitize_html_opt(&self.summary),
banned: Some(true),
expires,
};

View File

@ -17,7 +17,7 @@ use activitypub_federation::{
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{context::LemmyContext, utils::sanitize_html_opt};
use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
@ -117,7 +117,7 @@ impl ActivityHandler for UndoBlockUser {
let form = ModBanForm {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
reason: self.object.summary,
reason: sanitize_html_opt(&self.object.summary),
banned: Some(false),
expires,
};
@ -136,7 +136,7 @@ impl ActivityHandler for UndoBlockUser {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
community_id: community.id,
reason: self.object.summary,
reason: sanitize_html_opt(&self.object.summary),
banned: Some(false),
expires,
};

View File

@ -51,17 +51,19 @@ impl ActivityHandler for RawAnnouncableActivities {
if let AnnouncableActivities::Page(_) = activity {
return Err(LemmyErrorType::CannotReceivePage)?;
}
let community = activity.community(data).await?;
let actor_id = activity.actor().clone().into();
// verify and receive activity
activity.verify(data).await?;
activity.receive(data).await?;
activity.clone().receive(data).await?;
// send to community followers
if community.local {
verify_person_in_community(&actor_id, &community, data).await?;
AnnounceActivity::send(self, &community, data).await?;
// if activity is in a community, send to followers
let community = activity.community(data).await;
if let Ok(community) = community {
if community.local {
let actor_id = activity.actor().clone().into();
verify_person_in_community(&actor_id, &community, data).await?;
AnnounceActivity::send(self, &community, data).await?;
}
}
Ok(())
}

View File

@ -16,7 +16,7 @@ use lemmy_api_common::{
comment::{CommentReportResponse, CreateCommentReport},
context::LemmyContext,
post::{CreatePostReport, PostReportResponse},
utils::local_user_view_from_jwt,
utils::{local_user_view_from_jwt, sanitize_html},
};
use lemmy_db_schema::{
source::{
@ -132,7 +132,7 @@ impl ActivityHandler for Report {
post_id: post.id,
original_post_name: post.name.clone(),
original_post_url: post.url.clone(),
reason: self.summary,
reason: sanitize_html(&self.summary),
original_post_body: post.body.clone(),
};
PostReport::report(&mut context.pool(), &report_form).await?;
@ -142,7 +142,7 @@ impl ActivityHandler for Report {
creator_id: actor.id,
comment_id: comment.id,
original_comment_text: comment.content.clone(),
reason: self.summary,
reason: sanitize_html(&self.summary),
};
CommentReport::report(&mut context.pool(), &report_form).await?;
}

View File

@ -25,7 +25,7 @@ use activitypub_federation::{
};
use lemmy_api_common::{
build_response::send_local_notifs,
comment::{CommentResponse, CreateComment, EditComment},
comment::{CommentResponse, EditComment},
context::LemmyContext,
utils::{check_post_deleted_or_removed, is_mod_or_admin},
};
@ -44,25 +44,6 @@ use lemmy_db_schema::{
use lemmy_utils::{error::LemmyError, utils::mention::scrape_text_for_mentions};
use url::Url;
#[async_trait::async_trait]
impl SendActivity for CreateComment {
type Response = CommentResponse;
async fn send_activity(
_request: &Self,
response: &Self::Response,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdateNote::send(
&response.comment_view.comment,
response.comment_view.creator.id,
CreateOrUpdateType::Create,
context,
)
.await
}
}
#[async_trait::async_trait]
impl SendActivity for EditComment {
type Response = CommentResponse;
@ -73,10 +54,10 @@ impl SendActivity for EditComment {
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdateNote::send(
&response.comment_view.comment,
response.comment_view.comment.clone(),
response.comment_view.creator.id,
CreateOrUpdateType::Update,
context,
context.reset_request_count(),
)
.await
}
@ -84,11 +65,11 @@ impl SendActivity for EditComment {
impl CreateOrUpdateNote {
#[tracing::instrument(skip(comment, person_id, kind, context))]
async fn send(
comment: &Comment,
pub(crate) async fn send(
comment: Comment,
person_id: PersonId,
kind: CreateOrUpdateType,
context: &Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<(), LemmyError> {
// TODO: might be helpful to add a comment method to retrieve community directly
let post_id = comment.post_id;
@ -103,7 +84,7 @@ impl CreateOrUpdateNote {
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
let note = ApubComment(comment.clone()).into_json(context).await?;
let note = ApubComment(comment).into_json(&context).await?;
let create_or_update = CreateOrUpdateNote {
actor: person.id().into(),
@ -131,12 +112,12 @@ impl CreateOrUpdateNote {
.collect();
let mut inboxes = ActivitySendTargets::empty();
for t in tagged_users {
let person = t.dereference(context).await?;
let person = t.dereference(&context).await?;
inboxes.add_inbox(person.shared_inbox_or_inbox());
}
let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update);
send_activity_in_community(activity, &person, &community, inboxes, false, context).await
send_activity_in_community(activity, &person, &community, inboxes, false, &context).await
}
}

View File

@ -8,7 +8,7 @@ use crate::{
protocol::{activities::deletion::delete::Delete, IdOrNestedObject},
};
use activitypub_federation::{config::Data, kinds::activity::DeleteType, traits::ActivityHandler};
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{context::LemmyContext, utils::sanitize_html_opt};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentUpdateForm},
@ -105,6 +105,8 @@ pub(in crate::activities) async fn receive_remove_action(
reason: Option<String>,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let reason = sanitize_html_opt(&reason);
match DeletableObjects::read_from_db(object, context).await? {
DeletableObjects::Community(community) => {
if community.local {

View File

@ -1,6 +1,9 @@
use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::{create_or_update::page::CreateOrUpdatePage, CreateOrUpdateType},
protocol::activities::{
create_or_update::{note::CreateOrUpdateNote, page::CreateOrUpdatePage},
CreateOrUpdateType,
},
CONTEXT,
};
use activitypub_federation::{
@ -203,15 +206,17 @@ pub async fn match_outgoing_activities(
data: SendActivityData,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let fed_task = match data {
SendActivityData::CreatePost(post) => {
let creator_id = post.creator_id;
CreateOrUpdatePage::send(
post,
creator_id,
CreateOrUpdateType::Create,
context.reset_request_count(),
)
let context = context.reset_request_count();
let fed_task = async {
match data {
SendActivityData::CreatePost(post) => {
let creator_id = post.creator_id;
CreateOrUpdatePage::send(post, creator_id, CreateOrUpdateType::Create, context).await
}
SendActivityData::CreateComment(comment) => {
let creator_id = comment.creator_id;
CreateOrUpdateNote::send(comment, creator_id, CreateOrUpdateType::Create, context).await
}
}
};
if *SYNCHRONOUS_FEDERATION {

View File

@ -24,24 +24,32 @@ use crate::{
InCommunity,
},
};
use activitypub_federation::{
config::Data,
protocol::context::WithContext,
traits::ActivityHandler,
};
use activitypub_federation::{config::Data, traits::ActivityHandler};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
use url::Url;
/// List of activities which the shared inbox can handle.
///
/// This could theoretically be defined as an enum with variants `GroupInboxActivities` and
/// `PersonInboxActivities`. In practice we need to write it out manually so that priorities
/// are handled correctly.
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)]
pub enum SharedInboxActivities {
PersonInboxActivities(Box<WithContext<PersonInboxActivities>>),
GroupInboxActivities(Box<WithContext<GroupInboxActivities>>),
Follow(Follow),
AcceptFollow(AcceptFollow),
UndoFollow(UndoFollow),
CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage),
Report(Report),
AnnounceActivity(AnnounceActivity),
/// This is a catch-all and needs to be last
RawAnnouncableActivities(RawAnnouncableActivities),
}
/// List of activities which the group inbox can handle.
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)]
@ -49,10 +57,11 @@ pub enum GroupInboxActivities {
Follow(Follow),
UndoFollow(UndoFollow),
Report(Report),
// This is a catch-all and needs to be last
/// This is a catch-all and needs to be last
AnnouncableActivities(RawAnnouncableActivities),
}
/// List of activities which the person inbox can handle.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)]
@ -64,17 +73,8 @@ pub enum PersonInboxActivities {
Delete(Delete),
UndoDelete(UndoDelete),
AnnounceActivity(AnnounceActivity),
}
/// This is necessary for user inbox, which can also receive some "announcable" activities,
/// eg a comment mention. This needs to be a separate enum so that announcables received in shared
/// inbox can fall through to be parsed as GroupInboxActivities::AnnouncableActivities.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)]
pub enum PersonInboxActivitiesWithAnnouncable {
PersonInboxActivities(Box<PersonInboxActivities>),
AnnouncableActivities(Box<AnnouncableActivities>),
/// User can also receive some "announcable" activities, eg a comment mention.
AnnouncableActivities(AnnouncableActivities),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@ -138,12 +138,7 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use crate::{
activity_lists::{
GroupInboxActivities,
PersonInboxActivities,
PersonInboxActivitiesWithAnnouncable,
SiteInboxActivities,
},
activity_lists::{GroupInboxActivities, PersonInboxActivities, SiteInboxActivities},
protocol::tests::{test_json, test_parse_lemmy_item},
};
@ -161,16 +156,15 @@ mod tests {
fn test_person_inbox() {
test_parse_lemmy_item::<PersonInboxActivities>("assets/lemmy/activities/following/accept.json")
.unwrap();
test_parse_lemmy_item::<PersonInboxActivitiesWithAnnouncable>(
test_parse_lemmy_item::<PersonInboxActivities>(
"assets/lemmy/activities/create_or_update/create_note.json",
)
.unwrap();
test_parse_lemmy_item::<PersonInboxActivitiesWithAnnouncable>(
test_parse_lemmy_item::<PersonInboxActivities>(
"assets/lemmy/activities/create_or_update/create_private_message.json",
)
.unwrap();
test_json::<PersonInboxActivitiesWithAnnouncable>("assets/mastodon/activities/follow.json")
.unwrap();
test_json::<PersonInboxActivities>("assets/mastodon/activities/follow.json").unwrap();
}
#[test]

View File

@ -36,6 +36,8 @@ pub async fn list_posts(
};
let saved_only = data.saved_only;
let moderator_view = data.moderator_view;
let listing_type = Some(listing_type_with_default(
data.type_,
&local_site,
@ -48,6 +50,7 @@ pub async fn list_posts(
sort,
community_id,
saved_only,
moderator_view,
page,
limit,
..Default::default()

View File

@ -16,7 +16,7 @@ use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType};
#[tracing::instrument(skip(context))]
pub async fn read_community(
pub async fn get_community(
data: Query<GetCommunity>,
context: Data<LemmyContext>,
) -> Result<Json<GetCommunityResponse>, LemmyError> {

View File

@ -1,11 +1,15 @@
use crate::fetcher::search::{search_query_to_object_id, SearchableObjects};
use crate::fetcher::search::{
search_query_to_object_id,
search_query_to_object_id_local,
SearchableObjects,
};
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use diesel::NotFound;
use lemmy_api_common::{
context::LemmyContext,
site::{ResolveObject, ResolveObjectResponse},
utils::{check_private_instance, local_user_view_from_jwt},
utils::{check_private_instance, local_user_view_from_jwt_opt},
};
use lemmy_db_schema::{newtypes::PersonId, source::local_site::LocalSite, utils::DbPool};
use lemmy_db_views::structs::{CommentView, PostView};
@ -17,14 +21,23 @@ pub async fn resolve_object(
data: Query<ResolveObject>,
context: Data<LemmyContext>,
) -> Result<Json<ResolveObjectResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
let local_site = LocalSite::read(&mut context.pool()).await?;
let person_id = local_user_view.person.id;
check_private_instance(&Some(local_user_view), &local_site)?;
check_private_instance(&local_user_view, &local_site)?;
let person_id = local_user_view.map(|v| v.person.id);
// If we get a valid personId back we can safely assume that the user is authenticated,
// if there's no personId then the JWT was missing or invalid.
let is_authenticated = person_id.is_some();
let res = if is_authenticated {
// user is fully authenticated; allow remote lookups as well.
search_query_to_object_id(&data.q, &context).await
} else {
// user isn't authenticated only allow a local search.
search_query_to_object_id_local(&data.q, &context).await
}
.with_lemmy_type(LemmyErrorType::CouldntFindObject)?;
let res = search_query_to_object_id(&data.q, &context)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindObject)?;
convert_response(res, person_id, &mut context.pool())
.await
.with_lemmy_type(LemmyErrorType::CouldntFindObject)
@ -32,7 +45,7 @@ pub async fn resolve_object(
async fn convert_response(
object: SearchableObjects,
user_id: PersonId,
user_id: Option<PersonId>,
pool: &mut DbPool<'_>,
) -> Result<Json<ResolveObjectResponse>, LemmyError> {
use SearchableObjects::*;
@ -45,15 +58,15 @@ async fn convert_response(
}
Community(c) => {
removed_or_deleted = c.deleted || c.removed;
res.community = Some(CommunityView::read(pool, c.id, Some(user_id), None).await?)
res.community = Some(CommunityView::read(pool, c.id, user_id, None).await?)
}
Post(p) => {
removed_or_deleted = p.deleted || p.removed;
res.post = Some(PostView::read(pool, p.id, Some(user_id), None).await?)
res.post = Some(PostView::read(pool, p.id, user_id, None).await?)
}
Comment(c) => {
removed_or_deleted = c.deleted || c.removed;
res.comment = Some(CommentView::read(pool, c.id, Some(user_id)).await?)
res.comment = Some(CommentView::read(pool, c.id, user_id).await?)
}
};
// if the object was deleted from database, dont return it

View File

@ -8,7 +8,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
source::{community::Community, local_site::LocalSite},
utils::post_to_comment_sort_type,
utils::{post_to_comment_sort_type, post_to_person_sort_type},
SearchType,
};
use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery};
@ -98,7 +98,7 @@ pub async fn search(
}
SearchType::Users => {
users = PersonQuery {
sort: (sort),
sort: (sort.map(post_to_person_sort_type)),
search_term: (Some(q)),
page: (page),
limit: (limit),
@ -168,7 +168,7 @@ pub async fn search(
vec![]
} else {
PersonQuery {
sort: (sort),
sort: (sort.map(post_to_person_sort_type)),
search_term: (Some(q)),
page: (page),
limit: (limit),

View File

@ -44,6 +44,18 @@ pub(crate) async fn search_query_to_object_id(
})
}
/// Converts a search query to an object id. The query MUST bbe a URL which will bbe treated
/// as the ObjectId directly. If the query is a webfinger identifier (@user@example.com or
/// !community@example.com) this method will return an error.
#[tracing::instrument(skip_all)]
pub(crate) async fn search_query_to_object_id_local(
query: &str,
context: &Data<LemmyContext>,
) -> Result<SearchableObjects, LemmyError> {
let url = Url::parse(query)?;
ObjectId::from(url).dereference_local(context).await
}
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[derive(Debug)]
pub(crate) enum SearchableObjects {

View File

@ -1,5 +1,5 @@
use crate::{
activity_lists::PersonInboxActivitiesWithAnnouncable,
activity_lists::PersonInboxActivities,
fetcher::user_or_community::UserOrCommunity,
http::{create_apub_response, create_apub_tombstone_response},
objects::person::ApubPerson,
@ -49,7 +49,7 @@ pub async fn person_inbox(
body: Bytes,
data: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
receive_activity::<WithContext<PersonInboxActivitiesWithAnnouncable>, UserOrCommunity, LemmyContext>(
receive_activity::<WithContext<PersonInboxActivities>, UserOrCommunity, LemmyContext>(
request, body, &data,
)
.await

View File

@ -42,7 +42,21 @@ impl UrlVerifier for VerifyUrlData {
let local_site_data = local_site_data_cached(&mut (&self.0).into())
.await
.expect("read local site data");
check_apub_id_valid(url, &local_site_data)?;
check_apub_id_valid(url, &local_site_data).map_err(|err| match err {
LemmyError {
error_type: LemmyErrorType::FederationDisabled,
..
} => "Federation disabled",
LemmyError {
error_type: LemmyErrorType::DomainBlocked(_),
..
} => "Domain is blocked",
LemmyError {
error_type: LemmyErrorType::DomainNotInAllowList(_),
..
} => "Domain is not in allowlist",
_ => "Failed validating apub id",
})?;
Ok(())
}
}
@ -55,7 +69,7 @@ impl UrlVerifier for VerifyUrlData {
/// - URL being in the allowlist (if it is active)
/// - URL not being in the blocklist (if it is active)
#[tracing::instrument(skip(local_site_data))]
fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result<(), &'static str> {
fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result<(), LemmyError> {
let domain = apub_id.domain().expect("apud id has domain").to_string();
if !local_site_data
@ -64,7 +78,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result
.map(|l| l.federation_enabled)
.unwrap_or(true)
{
return Err("Federation disabled");
return Err(LemmyErrorType::FederationDisabled)?;
}
if local_site_data
@ -72,7 +86,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result
.iter()
.any(|i| domain.eq(&i.domain))
{
return Err("Domain is blocked");
return Err(LemmyErrorType::DomainBlocked(domain))?;
}
// Only check this if there are instances in the allowlist
@ -82,7 +96,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result
.iter()
.any(|i| domain.eq(&i.domain))
{
return Err("Domain is not in allowlist");
return Err(LemmyErrorType::DomainNotInAllowList(domain))?;
}
Ok(())
@ -142,12 +156,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
}
let local_site_data = local_site_data_cached(&mut context.pool()).await?;
check_apub_id_valid(apub_id, &local_site_data).map_err(|err| match err {
"Federation disabled" => LemmyErrorType::FederationDisabled,
"Domain is blocked" => LemmyErrorType::DomainBlocked,
"Domain is not in allowlist" => LemmyErrorType::DomainNotInAllowList,
_ => panic!("Could not handle apub error!"),
})?;
check_apub_id_valid(apub_id, &local_site_data)?;
// Only check allowlist if this is a community, and there are instances in the allowlist
if is_strict && !local_site_data.allowed_instances.is_empty() {

View File

@ -16,7 +16,10 @@ use activitypub_federation::{
traits::Object,
};
use chrono::NaiveDateTime;
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html},
};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentInsertForm, CommentUpdateForm},
@ -154,14 +157,15 @@ impl Object for ApubComment {
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let content_slurs_removed = remove_slurs(&content, slur_regex);
let content = remove_slurs(&content, slur_regex);
let content = sanitize_html(&content);
let language_id =
LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?;
let form = CommentInsertForm {
creator_id: creator.id,
post_id: post.id,
content: content_slurs_removed,
content,
removed: None,
published: note.published.map(|u| u.naive_local()),
updated: note.updated.map(|u| u.naive_local()),

View File

@ -17,7 +17,10 @@ use activitypub_federation::{
traits::{Actor, Object},
};
use chrono::NaiveDateTime;
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html_opt},
};
use lemmy_db_schema::{
newtypes::InstanceId,
source::{
@ -131,13 +134,17 @@ impl Object for ApubSite {
let domain = apub.id.inner().domain().expect("group id has domain");
let instance = DbInstance::read_or_create(&mut data.pool(), domain.to_string()).await?;
let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.source);
let sidebar = sanitize_html_opt(&sidebar);
let description = sanitize_html_opt(&apub.summary);
let site_form = SiteInsertForm {
name: apub.name.clone(),
sidebar: read_from_string_or_source_opt(&apub.content, &None, &apub.source),
sidebar,
updated: apub.updated.map(|u| u.clone().naive_local()),
icon: apub.icon.clone().map(|i| i.url.into()),
banner: apub.image.clone().map(|i| i.url.into()),
description: apub.summary.clone(),
description,
actor_id: Some(apub.id.clone().into()),
last_refreshed_at: Some(naive_now()),
inbox_url: Some(apub.inbox.clone().into()),

View File

@ -20,7 +20,7 @@ use activitypub_federation::{
use chrono::NaiveDateTime;
use lemmy_api_common::{
context::LemmyContext,
utils::{generate_outbox_url, local_site_opt_to_slur_regex},
utils::{generate_outbox_url, local_site_opt_to_slur_regex, sanitize_html, sanitize_html_opt},
};
use lemmy_db_schema::{
source::{
@ -142,12 +142,17 @@ impl Object for ApubPerson {
) -> Result<ApubPerson, LemmyError> {
let instance_id = fetch_instance_actor_for_object(&person.id, context).await?;
let name = sanitize_html(&person.preferred_username);
let display_name = sanitize_html_opt(&person.name);
let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source);
let bio = sanitize_html_opt(&bio);
// Some Mastodon users have `name: ""` (empty string), need to convert that to `None`
// https://github.com/mastodon/mastodon/issues/25233
let display_name = person.name.filter(|n| !n.is_empty());
let display_name = display_name.filter(|n| !n.is_empty());
let person_form = PersonInsertForm {
name: person.preferred_username,
name,
display_name,
banned: None,
ban_expires: None,
@ -157,7 +162,7 @@ impl Object for ApubPerson {
published: person.published.map(|u| u.naive_local()),
updated: person.updated.map(|u| u.naive_local()),
actor_id: Some(person.id.into()),
bio: read_from_string_or_source_opt(&person.summary, &None, &person.source),
bio,
local: Some(false),
admin: Some(false),
bot_account: Some(person.kind == UserTypes::Service),

View File

@ -25,7 +25,13 @@ use html2md::parse_html;
use lemmy_api_common::{
context::LemmyContext,
request::fetch_site_data,
utils::{is_mod_or_admin, local_site_opt_to_sensitive, local_site_opt_to_slur_regex},
utils::{
is_mod_or_admin,
local_site_opt_to_sensitive,
local_site_opt_to_slur_regex,
sanitize_html,
sanitize_html_opt,
},
};
use lemmy_db_schema::{
self,
@ -228,6 +234,10 @@ impl Object for ApubPost {
let language_id =
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?;
let name = sanitize_html(&name);
let embed_title = sanitize_html_opt(&embed_title);
let embed_description = sanitize_html_opt(&embed_description);
PostInsertForm {
name,
url: url.map(Into::into),

View File

@ -12,7 +12,10 @@ use activitypub_federation::{
traits::Object,
};
use chrono::NaiveDateTime;
use lemmy_api_common::{context::LemmyContext, utils::check_person_block};
use lemmy_api_common::{
context::LemmyContext,
utils::{check_person_block, sanitize_html},
};
use lemmy_db_schema::{
source::{
person::Person,
@ -118,10 +121,13 @@ impl Object for ApubPrivateMessage {
let recipient = note.to[0].dereference(context).await?;
check_person_block(creator.id, recipient.id, &mut context.pool()).await?;
let content = read_from_string_or_source(&note.content, &None, &note.source);
let content = sanitize_html(&content);
let form = PrivateMessageInsertForm {
creator_id: creator.id,
recipient_id: recipient.id,
content: read_from_string_or_source(&note.content, &None, &note.source),
content,
published: note.published.map(|u| u.naive_local()),
updated: note.updated.map(|u| u.naive_local()),
deleted: Some(false),

View File

@ -23,7 +23,10 @@ use activitypub_federation::{
},
};
use chrono::{DateTime, FixedOffset};
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html, sanitize_html_opt},
};
use lemmy_db_schema::{
newtypes::InstanceId,
source::community::{CommunityInsertForm, CommunityUpdateForm},
@ -94,10 +97,15 @@ impl Group {
}
pub(crate) fn into_insert_form(self, instance_id: InstanceId) -> CommunityInsertForm {
let name = sanitize_html(&self.preferred_username);
let title = sanitize_html(&self.name.unwrap_or(self.preferred_username));
let description = read_from_string_or_source_opt(&self.summary, &None, &self.source);
let description = sanitize_html_opt(&description);
CommunityInsertForm {
name: self.preferred_username.clone(),
title: self.name.unwrap_or(self.preferred_username),
description: read_from_string_or_source_opt(&self.summary, &None, &self.source),
name,
title,
description,
removed: None,
published: self.published.map(|u| u.naive_local()),
updated: self.updated.map(|u| u.naive_local()),

View File

@ -161,7 +161,8 @@ mod tests {
.await
.unwrap();
assert_eq!(0, after_parent_comment_removed.comment_count);
assert_eq!(0, after_parent_comment_removed.comment_score);
// TODO: fix person aggregate comment score calculation
// assert_eq!(0, after_parent_comment_removed.comment_score);
// Remove a parent comment (the scores should also be removed)
Comment::delete(pool, inserted_comment.id).await.unwrap();
@ -172,7 +173,8 @@ mod tests {
.await
.unwrap();
assert_eq!(0, after_parent_comment_delete.comment_count);
assert_eq!(0, after_parent_comment_delete.comment_score);
// TODO: fix person aggregate comment score calculation
// assert_eq!(0, after_parent_comment_delete.comment_score);
// Add in the two comments again, then delete the post.
let new_parent_comment = Comment::create(pool, &comment_form, None).await.unwrap();
@ -186,13 +188,15 @@ mod tests {
.await
.unwrap();
assert_eq!(2, after_comment_add.comment_count);
assert_eq!(1, after_comment_add.comment_score);
// TODO: fix person aggregate comment score calculation
// assert_eq!(1, after_comment_add.comment_score);
Post::delete(pool, inserted_post.id).await.unwrap();
let after_post_delete = PersonAggregates::read(pool, inserted_person.id)
.await
.unwrap();
assert_eq!(0, after_post_delete.comment_score);
// TODO: fix person aggregate comment score calculation
// assert_eq!(0, after_post_delete.comment_score);
assert_eq!(0, after_post_delete.comment_count);
assert_eq!(0, after_post_delete.post_score);
assert_eq!(0, after_post_delete.post_count);

View File

@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "full")]
use ts_rs::TS;
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable, TS))]
#[cfg_attr(feature = "full", diesel(table_name = comment_aggregates))]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))]
@ -28,6 +28,7 @@ pub struct CommentAggregates {
/// The total number of children in this comment branch.
pub child_count: i32,
pub hot_rank: i32,
pub controversy_rank: f64,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
@ -72,7 +73,7 @@ pub struct PersonAggregates {
pub comment_score: i64,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable, TS))]
#[cfg_attr(feature = "full", diesel(table_name = post_aggregates))]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))]
@ -98,6 +99,7 @@ pub struct PostAggregates {
pub hot_rank_active: i32,
pub community_id: CommunityId,
pub creator_id: PersonId,
pub controversy_rank: f64,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]

View File

@ -1,7 +1,7 @@
use crate::{
diesel::dsl::IntervalDsl,
newtypes::InstanceId,
schema::{federation_allowlist, federation_blocklist, instance},
schema::{federation_allowlist, federation_blocklist, instance, local_site, site},
source::instance::{Instance, InstanceForm},
utils::{get_conn, naive_now, DbPool},
};
@ -130,6 +130,10 @@ impl Instance {
pub async fn linked(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
instance::table
// omit instance representing the local site
.left_join(site::table.inner_join(local_site::table))
.filter(local_site::id.is_null())
// omit instances in the blocklist
.left_join(federation_blocklist::table)
.filter(federation_blocklist::id.is_null())
.select(instance::all_columns)

View File

@ -28,6 +28,11 @@ pub mod newtypes;
#[rustfmt::skip]
#[allow(clippy::wildcard_imports)]
pub mod schema;
#[cfg(feature = "full")]
pub mod aliases {
use crate::schema::person;
diesel::alias!(person as person1: Person1, person as person2: Person2);
}
pub mod source;
#[cfg(feature = "full")]
pub mod traits;
@ -66,6 +71,7 @@ pub enum SortType {
TopThreeMonths,
TopSixMonths,
TopNineMonths,
Controversial,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)]
@ -77,6 +83,20 @@ pub enum CommentSortType {
Top,
New,
Old,
Controversial,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The person sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html
pub enum PersonSortType {
New,
Old,
MostComments,
CommentScore,
PostScore,
PostCount,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]

View File

@ -97,6 +97,7 @@ diesel::table! {
published -> Timestamp,
child_count -> Int4,
hot_rank -> Int4,
controversy_rank -> Float8,
}
}
@ -408,6 +409,8 @@ diesel::table! {
totp_2fa_secret -> Nullable<Text>,
totp_2fa_url -> Nullable<Text>,
open_links_in_new_tab -> Bool,
blur_nsfw -> Bool,
auto_expand -> Bool,
infinite_scroll_enabled -> Bool,
}
}
@ -687,6 +690,7 @@ diesel::table! {
hot_rank_active -> Int4,
community_id -> Int4,
creator_id -> Int4,
controversy_rank -> Float8,
}
}

View File

@ -53,6 +53,8 @@ pub struct LocalUser {
pub totp_2fa_url: Option<String>,
/// Open links in a new tab.
pub open_links_in_new_tab: bool,
pub blur_nsfw: bool,
pub auto_expand: bool,
/// Whether infinite scroll is enabled.
pub infinite_scroll_enabled: bool,
}
@ -83,6 +85,8 @@ pub struct LocalUserInsertForm {
pub totp_2fa_secret: Option<Option<String>>,
pub totp_2fa_url: Option<Option<String>>,
pub open_links_in_new_tab: Option<bool>,
pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
pub infinite_scroll_enabled: Option<bool>,
}
@ -109,5 +113,7 @@ pub struct LocalUserUpdateForm {
pub totp_2fa_secret: Option<Option<String>>,
pub totp_2fa_url: Option<Option<String>>,
pub open_links_in_new_tab: Option<bool>,
pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
pub infinite_scroll_enabled: Option<bool>,
}

View File

@ -2,7 +2,9 @@ use crate::{
diesel::Connection,
diesel_migrations::MigrationHarness,
newtypes::DbUrl,
traits::JoinView,
CommentSortType,
PersonSortType,
SortType,
};
use activitypub_federation::{fetch::object_id::ObjectId, traits::Object};
@ -25,7 +27,7 @@ use diesel_async::{
},
};
use diesel_migrations::EmbeddedMigrations;
use futures_util::{future::BoxFuture, FutureExt};
use futures_util::{future::BoxFuture, Future, FutureExt};
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
settings::structs::Settings,
@ -196,12 +198,12 @@ pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
pub fn diesel_option_overwrite(opt: Option<String>) -> Option<Option<String>> {
match opt {
// An empty string is an erase
Some(unwrapped) => {
if !unwrapped.eq("") {
Some(Some(unwrapped.clone()))
Some(Some(unwrapped))
} else {
Some(None)
}
@ -347,6 +349,7 @@ pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType {
SortType::Active | SortType::Hot => CommentSortType::Hot,
SortType::New | SortType::NewComments | SortType::MostComments => CommentSortType::New,
SortType::Old => CommentSortType::Old,
SortType::Controversial => CommentSortType::Controversial,
SortType::TopHour
| SortType::TopSixHour
| SortType::TopTwelveHour
@ -361,6 +364,16 @@ pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType {
}
}
pub fn post_to_person_sort_type(sort: SortType) -> PersonSortType {
match sort {
SortType::Active | SortType::Hot | SortType::Controversial => PersonSortType::CommentScore,
SortType::New | SortType::NewComments => PersonSortType::New,
SortType::MostComments => PersonSortType::MostComments,
SortType::Old => PersonSortType::Old,
_ => PersonSortType::CommentScore,
}
}
static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
.expect("compile email regex")
@ -373,6 +386,10 @@ pub mod functions {
fn hot_rank(score: BigInt, time: Timestamp) -> Integer;
}
sql_function! {
fn controversy_rank(upvotes: BigInt, downvotes: BigInt, score: BigInt) -> Double;
}
sql_function!(fn lower(x: Text) -> Text);
// really this function is variadic, this just adds the two-argument version
@ -407,6 +424,94 @@ where
}
}
pub type ResultFuture<'a, T> = BoxFuture<'a, Result<T, DieselError>>;
pub trait ReadFn<'a, T: JoinView, Args>:
Fn(DbConn<'a>, Args) -> ResultFuture<'a, <T as JoinView>::JoinTuple>
{
}
impl<
'a,
T: JoinView,
Args,
F: Fn(DbConn<'a>, Args) -> ResultFuture<'a, <T as JoinView>::JoinTuple>,
> ReadFn<'a, T, Args> for F
{
}
pub trait ListFn<'a, T: JoinView, Args>:
Fn(DbConn<'a>, Args) -> ResultFuture<'a, Vec<<T as JoinView>::JoinTuple>>
{
}
impl<
'a,
T: JoinView,
Args,
F: Fn(DbConn<'a>, Args) -> ResultFuture<'a, Vec<<T as JoinView>::JoinTuple>>,
> ListFn<'a, T, Args> for F
{
}
/// Allows read and list functions to capture a shared closure that has an inferred return type, which is useful for join logic
pub struct Queries<RF, LF> {
pub read_fn: RF,
pub list_fn: LF,
}
// `()` is used to prevent type inference error
impl Queries<(), ()> {
pub fn new<'a, RFut, LFut, RT, LT, RA, LA, RF2, LF2>(
read_fn: RF2,
list_fn: LF2,
) -> Queries<impl ReadFn<'a, RT, RA>, impl ListFn<'a, LT, LA>>
where
RFut: Future<Output = Result<<RT as JoinView>::JoinTuple, DieselError>> + Sized + Send + 'a,
LFut:
Future<Output = Result<Vec<<LT as JoinView>::JoinTuple>, DieselError>> + Sized + Send + 'a,
RT: JoinView,
LT: JoinView,
RF2: Fn(DbConn<'a>, RA) -> RFut,
LF2: Fn(DbConn<'a>, LA) -> LFut,
{
Queries {
read_fn: move |conn, args| read_fn(conn, args).boxed(),
list_fn: move |conn, args| list_fn(conn, args).boxed(),
}
}
}
impl<RF, LF> Queries<RF, LF> {
pub async fn read<'a, T, Args>(
self,
pool: &'a mut DbPool<'_>,
args: Args,
) -> Result<T, DieselError>
where
T: JoinView,
RF: ReadFn<'a, T, Args>,
{
let conn = get_conn(pool).await?;
let res = (self.read_fn)(conn, args).await?;
Ok(T::from_tuple(res))
}
pub async fn list<'a, T, Args>(
self,
pool: &'a mut DbPool<'_>,
args: Args,
) -> Result<Vec<T>, DieselError>
where
T: JoinView,
LF: ListFn<'a, T, Args>,
{
let conn = get_conn(pool).await?;
let res = (self.list_fn)(conn, args).await?;
Ok(res.into_iter().map(T::from_tuple).collect())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
@ -432,10 +537,10 @@ mod tests {
#[test]
fn test_diesel_option_overwrite() {
assert_eq!(diesel_option_overwrite(&None), None);
assert_eq!(diesel_option_overwrite(&Some(String::new())), Some(None));
assert_eq!(diesel_option_overwrite(None), None);
assert_eq!(diesel_option_overwrite(Some(String::new())), Some(None));
assert_eq!(
diesel_option_overwrite(&Some("test".to_string())),
diesel_option_overwrite(Some("test".to_string())),
Some(Some("test".to_string()))
);
}

View File

@ -1,6 +1,7 @@
use crate::structs::CommentReportView;
use diesel::{
dsl::now,
pg::Pg,
result::Error,
BoolExpressionMethods,
ExpressionMethods,
@ -11,6 +12,7 @@ use diesel::{
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
aggregates::structs::CommentAggregates,
aliases,
newtypes::{CommentReportId, CommunityId, PersonId},
schema::{
comment,
@ -31,9 +33,119 @@ use lemmy_db_schema::{
post::Post,
},
traits::JoinView,
utils::{get_conn, limit_and_offset, DbPool},
utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
};
fn queries<'a>() -> Queries<
impl ReadFn<'a, CommentReportView, (CommentReportId, PersonId)>,
impl ListFn<'a, CommentReportView, (CommentReportQuery, &'a Person)>,
> {
let all_joins = |query: comment_report::BoxedQuery<'a, Pg>, my_person_id: PersonId| {
query
.inner_join(comment::table)
.inner_join(post::table.on(comment::post_id.eq(post::id)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person::table.on(comment_report::creator_id.eq(person::id)))
.inner_join(aliases::person1.on(comment::creator_id.eq(aliases::person1.field(person::id))))
.inner_join(
comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)),
)
.left_join(
comment_like::table.on(
comment::id
.eq(comment_like::comment_id)
.and(comment_like::person_id.eq(my_person_id)),
),
)
.left_join(
aliases::person2
.on(comment_report::resolver_id.eq(aliases::person2.field(person::id).nullable())),
)
};
let selection = (
comment_report::all_columns,
comment::all_columns,
post::all_columns,
community::all_columns,
person::all_columns,
aliases::person1.fields(person::all_columns),
comment_aggregates::all_columns,
community_person_ban::all_columns.nullable(),
comment_like::score.nullable(),
aliases::person2.fields(person::all_columns).nullable(),
);
let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (CommentReportId, PersonId)| async move {
all_joins(
comment_report::table.find(report_id).into_boxed(),
my_person_id,
)
.left_join(
community_person_ban::table.on(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(comment::creator_id)),
),
)
.select(selection)
.first::<<CommentReportView as JoinView>::JoinTuple>(&mut conn)
.await
};
let list = move |mut conn: DbConn<'a>, (options, my_person): (CommentReportQuery, &'a Person)| async move {
let mut query = all_joins(comment_report::table.into_boxed(), my_person.id)
.left_join(
community_person_ban::table.on(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(comment::creator_id))
.and(
community_person_ban::expires
.is_null()
.or(community_person_ban::expires.gt(now)),
),
),
)
.select(selection);
if let Some(community_id) = options.community_id {
query = query.filter(post::community_id.eq(community_id));
}
if options.unresolved_only.unwrap_or(false) {
query = query.filter(comment_report::resolved.eq(false));
}
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
query = query
.order_by(comment_report::published.desc())
.limit(limit)
.offset(offset);
// If its not an admin, get only the ones you mod
if !my_person.admin {
query
.inner_join(
community_moderator::table.on(
community_moderator::community_id
.eq(post::community_id)
.and(community_moderator::person_id.eq(my_person.id)),
),
)
.load::<<CommentReportView as JoinView>::JoinTuple>(&mut conn)
.await
} else {
query
.load::<<CommentReportView as JoinView>::JoinTuple>(&mut conn)
.await
}
};
Queries::new(read, list)
}
impl CommentReportView {
/// returns the CommentReportView for the provided report_id
///
@ -43,54 +155,7 @@ impl CommentReportView {
report_id: CommentReportId,
my_person_id: PersonId,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let (person_alias_1, person_alias_2) = diesel::alias!(person as person1, person as person2);
let res = comment_report::table
.find(report_id)
.inner_join(comment::table)
.inner_join(post::table.on(comment::post_id.eq(post::id)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person::table.on(comment_report::creator_id.eq(person::id)))
.inner_join(person_alias_1.on(comment::creator_id.eq(person_alias_1.field(person::id))))
.inner_join(
comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)),
)
.left_join(
community_person_ban::table.on(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(comment::creator_id)),
),
)
.left_join(
comment_like::table.on(
comment::id
.eq(comment_like::comment_id)
.and(comment_like::person_id.eq(my_person_id)),
),
)
.left_join(
person_alias_2
.on(comment_report::resolver_id.eq(person_alias_2.field(person::id).nullable())),
)
.select((
comment_report::all_columns,
comment::all_columns,
post::all_columns,
community::all_columns,
person::all_columns,
person_alias_1.fields(person::all_columns),
comment_aggregates::all_columns,
community_person_ban::all_columns.nullable(),
comment_like::score.nullable(),
person_alias_2.fields(person::all_columns).nullable(),
))
.first::<<CommentReportView as JoinView>::JoinTuple>(conn)
.await?;
Ok(Self::from_tuple(res))
queries().read(pool, (report_id, my_person_id)).await
}
/// Returns the current unresolved post report count for the communities you mod
@ -150,90 +215,7 @@ impl CommentReportQuery {
pool: &mut DbPool<'_>,
my_person: &Person,
) -> Result<Vec<CommentReportView>, Error> {
let conn = &mut get_conn(pool).await?;
let (person_alias_1, person_alias_2) = diesel::alias!(person as person1, person as person2);
let mut query = comment_report::table
.inner_join(comment::table)
.inner_join(post::table.on(comment::post_id.eq(post::id)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person::table.on(comment_report::creator_id.eq(person::id)))
.inner_join(person_alias_1.on(comment::creator_id.eq(person_alias_1.field(person::id))))
.inner_join(
comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)),
)
.left_join(
community_person_ban::table.on(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(comment::creator_id))
.and(
community_person_ban::expires
.is_null()
.or(community_person_ban::expires.gt(now)),
),
),
)
.left_join(
comment_like::table.on(
comment::id
.eq(comment_like::comment_id)
.and(comment_like::person_id.eq(my_person.id)),
),
)
.left_join(
person_alias_2
.on(comment_report::resolver_id.eq(person_alias_2.field(person::id).nullable())),
)
.select((
comment_report::all_columns,
comment::all_columns,
post::all_columns,
community::all_columns,
person::all_columns,
person_alias_1.fields(person::all_columns),
comment_aggregates::all_columns,
community_person_ban::all_columns.nullable(),
comment_like::score.nullable(),
person_alias_2.fields(person::all_columns).nullable(),
))
.into_boxed();
if let Some(community_id) = self.community_id {
query = query.filter(post::community_id.eq(community_id));
}
if self.unresolved_only.unwrap_or(false) {
query = query.filter(comment_report::resolved.eq(false));
}
let (limit, offset) = limit_and_offset(self.page, self.limit)?;
query = query
.order_by(comment_report::published.desc())
.limit(limit)
.offset(offset);
// If its not an admin, get only the ones you mod
let res = if !my_person.admin {
query
.inner_join(
community_moderator::table.on(
community_moderator::community_id
.eq(post::community_id)
.and(community_moderator::person_id.eq(my_person.id)),
),
)
.load::<<CommentReportView as JoinView>::JoinTuple>(conn)
.await?
} else {
query
.load::<<CommentReportView as JoinView>::JoinTuple>(conn)
.await?
};
Ok(res.into_iter().map(CommentReportView::from_tuple).collect())
queries().list(pool, (self, my_person)).await
}
}
@ -478,6 +460,7 @@ mod tests {
published: agg.published,
child_count: 0,
hot_rank: 1728,
controversy_rank: 0.0,
},
my_vote: None,
resolver: None,

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