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 * @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 # Database cluster and sockets for testing
dev_pgdata/ dev_pgdata/
*.PGSQL.* *.PGSQL.*
# database dumps
*.sqldump

View File

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

357
Cargo.lock generated
View File

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

View File

@ -89,7 +89,7 @@ tracing-error = "0.2.0"
tracing-log = "0.1.3" tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = { version = "2.4.0", features = ["serde"] } 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-middleware = "0.2.2"
reqwest-tracing = "0.4.5" reqwest-tracing = "0.4.5"
clokwerk = "0.4.0" clokwerk = "0.4.0"

View File

@ -112,8 +112,7 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
## Installation ## Installation
- [Docker](https://join-lemmy.org/docs/administration/install_docker.html) - [Lemmy Administration Docs](https://join-lemmy.org/docs/administration/administration.html)
- [Ansible](https://join-lemmy.org/docs/administration/install_ansible.html)
## Lemmy Projects ## 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) # Lemmy v0.18.1 Release (2023-07-07)
## What is Lemmy? ## What is Lemmy?

View File

@ -13,7 +13,7 @@ popd
yarn yarn
yarn api-test || true 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 for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
psql "$LEMMY_DATABASE_URL" -c "DROP DATABASE $INSTANCE" psql "$LEMMY_DATABASE_URL" -c "DROP DATABASE $INSTANCE"

View File

@ -112,8 +112,27 @@ test("Update a comment", async () => {
}); });
test("Delete 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); 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( let deleteCommentRes = await deleteComment(
alpha, alpha,
true, true,
@ -126,6 +145,12 @@ test("Delete a comment", async () => {
resolveComment(beta, commentRes.comment_view.comment), resolveComment(beta, commentRes.comment_view.comment),
).rejects.toBe("couldnt_find_object"); ).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( let undeleteCommentRes = await deleteComment(
alpha, alpha,
false, false,
@ -141,7 +166,7 @@ test("Delete a comment", async () => {
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view); 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); let commentRes = await createComment(alpha, postRes.post_view.post.id);
// Get the id for beta // 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 () => { test("Unlike a comment", async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); 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); let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment);
expect(unlike.comment_view.counts.score).toBe(0); 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 = ( let betaComment = (
await resolveComment(beta, commentRes.comment_view.comment) await resolveComment(beta, commentRes.comment_view.comment)
).comment; ).comment;
@ -236,6 +273,16 @@ test("Unlike a comment", async () => {
expect(betaComment?.community.local).toBe(true); expect(betaComment?.community.local).toBe(true);
expect(betaComment?.creator.local).toBe(false); expect(betaComment?.creator.local).toBe(false);
expect(betaComment?.counts.score).toBe(0); 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 () => { test("Federated comment like", async () => {

View File

@ -18,6 +18,9 @@ import {
createPost, createPost,
getPost, getPost,
resolvePost, resolvePost,
registerUser,
API,
getPosts,
} from "./shared"; } from "./shared";
beforeAll(async () => { 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); let gammaPost2 = await getPost(gamma, gammaPost.post.id);
expect(gammaPost2.post_view.creator_banned_from_community).toBe(false); 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, resolveCommunity,
} from "./shared"; } from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView"; import { PostView } from "lemmy-js-client/dist/types/PostView";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
let betaCommunity: CommunityView | undefined; let betaCommunity: CommunityView | undefined;
@ -412,7 +413,7 @@ test("Enforce site ban for federated user", async () => {
expect(alphaUserOnBeta2.person?.person.banned).toBe(false); 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) { if (!betaCommunity) {
throw "Missing beta community"; 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.original_post_body).toBe(alphaReport.original_post_body);
expect(betaReport.reason).toBe(alphaReport.reason); 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 { CreateCommentReport } from "lemmy-js-client/dist/types/CreateCommentReport";
import { ListCommentReportsResponse } from "lemmy-js-client/dist/types/ListCommentReportsResponse"; import { ListCommentReportsResponse } from "lemmy-js-client/dist/types/ListCommentReportsResponse";
import { ListCommentReports } from "lemmy-js-client/dist/types/ListCommentReports"; 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 { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDetailsResponse";
import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails"; 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> { export async function saveUserSettingsBio(api: API): Promise<LoginResponse> {
let form: SaveUserSettings = { let form: SaveUserSettings = {
show_nsfw: true, show_nsfw: true,
blur_nsfw: false,
auto_expand: true,
theme: "darkly", theme: "darkly",
default_sort_type: "Active", default_sort_type: "Active",
default_listing_type: "All", default_listing_type: "All",
@ -631,6 +635,8 @@ export async function saveUserSettingsFederated(
let bio = "a changed bio"; let bio = "a changed bio";
let form: SaveUserSettings = { let form: SaveUserSettings = {
show_nsfw: false, show_nsfw: false,
blur_nsfw: true,
auto_expand: false,
default_sort_type: "Hot", default_sort_type: "Hot",
default_listing_type: "All", default_listing_type: "All",
interface_language: "", interface_language: "",
@ -753,6 +759,17 @@ export async function listCommentReports(
return api.client.listCommentReports(form); 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) { export function delay(millis = 500) {
return new Promise(resolve => setTimeout(resolve, millis)); return new Promise(resolve => setTimeout(resolve, millis));
} }

View File

@ -2,6 +2,11 @@
# yarn lockfile v1 # 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": "@ampproject/remapping@^2.2.0":
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
@ -496,6 +501,13 @@
dependencies: dependencies:
"@sinclair/typebox" "^0.25.16" "@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": "@jest/source-map@^29.4.3":
version "29.4.3" version "29.4.3"
resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20" 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" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718"
integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== 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": "@sinonjs/commons@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" 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== integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cross-fetch@^3.1.5: cross-fetch@^3.1.5:
version "3.1.5" version "3.1.8"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
dependencies: dependencies:
node-fetch "2.6.7" node-fetch "^2.6.12"
cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "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" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
node-fetch@2.6.7: node-fetch@^2.6.12:
version "2.6.7" version "2.6.12"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"
@ -2310,9 +2327,9 @@ node-int64@^0.4.0:
integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
node-releases@^2.0.8: node-releases@^2.0.8:
version "2.0.10" version "2.0.13"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
normalize-path@^3.0.0: normalize-path@^3.0.0:
version "3.0.0" version "3.0.0"
@ -2341,16 +2358,16 @@ onetime@^5.1.2:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
optionator@^0.9.1: optionator@^0.9.1:
version "0.9.1" version "0.9.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==
dependencies: dependencies:
"@aashutoshrathi/word-wrap" "^1.2.3"
deep-is "^0.1.3" deep-is "^0.1.3"
fast-levenshtein "^2.0.6" fast-levenshtein "^2.0.6"
levn "^0.4.1" levn "^0.4.1"
prelude-ls "^1.2.1" prelude-ls "^1.2.1"
type-check "^0.4.0" type-check "^0.4.0"
word-wrap "^1.2.3"
p-limit@^2.2.0: p-limit@^2.2.0:
version "2.3.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== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pirates@^4.0.4: pirates@^4.0.4:
version "4.0.5" version "4.0.6"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
pkg-dir@^4.2.0: pkg-dir@^4.2.0:
version "4.2.0" version "4.2.0"
@ -2467,11 +2484,11 @@ prettier@^3.0.0:
integrity sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g== integrity sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==
pretty-format@^29.0.0, pretty-format@^29.5.0: pretty-format@^29.0.0, pretty-format@^29.5.0:
version "29.5.0" version "29.6.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.1.tgz#ec838c288850b7c4f9090b867c2d4f4edbfb0f3e"
integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw== integrity sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==
dependencies: dependencies:
"@jest/schemas" "^29.4.3" "@jest/schemas" "^29.6.0"
ansi-styles "^5.0.0" ansi-styles "^5.0.0"
react-is "^18.0.0" react-is "^18.0.0"
@ -2558,18 +2575,18 @@ run-parallel@^1.1.9:
dependencies: dependencies:
queue-microtask "^1.2.2" queue-microtask "^1.2.2"
semver@7.x, semver@^7.3.5, semver@^7.3.7: semver@^6.0.0, semver@^6.3.0:
version "7.5.0" version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== 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: dependencies:
lru-cache "^6.0.0" 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: shebang-command@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 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== integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
ts-jest@^29.1.0: ts-jest@^29.1.0:
version "29.1.0" version "29.1.1"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.0.tgz#4a9db4104a49b76d2b368ea775b6c9535c603891" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b"
integrity sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA== integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==
dependencies: dependencies:
bs-logger "0.x" bs-logger "0.x"
fast-json-stable-stringify "2.x" fast-json-stable-stringify "2.x"
@ -2734,7 +2751,7 @@ ts-jest@^29.1.0:
json5 "^2.2.3" json5 "^2.2.3"
lodash.memoize "4.x" lodash.memoize "4.x"
make-error "1.x" make-error "1.x"
semver "7.x" semver "^7.5.3"
yargs-parser "^21.0.1" yargs-parser "^21.0.1"
tslib@^1.8.1: tslib@^1.8.1:
@ -2772,9 +2789,9 @@ type-fest@^0.21.3:
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typescript@^5.0.4: typescript@^5.0.4:
version "5.0.4" version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
update-browserslist-db@^1.0.10: update-browserslist-db@^1.0.10:
version "1.0.11" version "1.0.11"
@ -2827,11 +2844,6 @@ which@^2.0.1:
dependencies: dependencies:
isexe "^2.0.0" 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: wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 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_moderator = { workspace = true, features = ["full"] }
lemmy_db_views_actor = { workspace = true, features = ["full"] } lemmy_db_views_actor = { workspace = true, features = ["full"] }
lemmy_api_common = { workspace = true, features = ["full"] } lemmy_api_common = { workspace = true, features = ["full"] }
activitypub_federation = { workspace = true }
bcrypt = { workspace = true } bcrypt = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
actix-web = { workspace = true } actix-web = { workspace = true }

View File

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

View File

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

View File

@ -1,5 +1,4 @@
use crate::Perform; use actix_web::web::{Data, Json};
use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
comment::{CommentResponse, SaveComment}, comment::{CommentResponse, SaveComment},
context::LemmyContext, context::LemmyContext,
@ -12,38 +11,35 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::CommentView; use lemmy_db_views::structs::CommentView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl Perform for SaveComment { pub async fn save_comment(
type Response = CommentResponse; 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))] let comment_saved_form = CommentSavedForm {
async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommentResponse, LemmyError> { comment_id: data.comment_id,
let data: &SaveComment = self; person_id: local_user_view.person.id,
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?; };
let comment_saved_form = CommentSavedForm { if data.save {
comment_id: data.comment_id, CommentSaved::save(&mut context.pool(), &comment_saved_form)
person_id: local_user_view.person.id, .await
}; .with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
} else {
if data.save { CommentSaved::unsave(&mut context.pool(), &comment_saved_form)
CommentSaved::save(&mut context.pool(), &comment_saved_form) .await
.await .with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
.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,
})
} }
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::{ use lemmy_api_common::{
comment::{CommentReportResponse, CreateCommentReport}, comment::{CommentReportResponse, CreateCommentReport},
context::LemmyContext, 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::{ use lemmy_db_schema::{
source::{ source::{
@ -29,8 +34,8 @@ impl Perform for CreateCommentReport {
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?; let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = self.reason.trim(); let reason = sanitize_html(self.reason.trim());
check_report_reason(reason, &local_site)?; check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_id = data.comment_id; let comment_id = data.comment_id;
@ -42,7 +47,7 @@ impl Perform for CreateCommentReport {
creator_id: person_id, creator_id: person_id,
comment_id, comment_id,
original_comment_text: comment_view.comment.content, original_comment_text: comment_view.comment.content,
reason: reason.to_owned(), reason,
}; };
let report = CommentReport::report(&mut context.pool(), &report_form) let report = CommentReport::report(&mut context.pool(), &report_form)

View File

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

View File

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

View File

@ -1,5 +1,4 @@
use crate::Perform; use actix_web::web::{Data, Json};
use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
comment::{CommentReportResponse, ResolveCommentReport}, comment::{CommentReportResponse, ResolveCommentReport},
context::LemmyContext, context::LemmyContext,
@ -10,41 +9,35 @@ use lemmy_db_views::structs::CommentReportView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
/// Resolves or unresolves a comment report and notifies the moderators of the community /// Resolves or unresolves a comment report and notifies the moderators of the community
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl Perform for ResolveCommentReport { pub async fn resolve_comment_report(
type Response = CommentReportResponse; 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))] let report_id = data.report_id;
async fn perform( let person_id = local_user_view.person.id;
&self, let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
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 person_id = local_user_view.person.id; is_mod_or_admin(&mut context.pool(), person_id, report.community.id).await?;
let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
let person_id = local_user_view.person.id; if data.resolved {
is_mod_or_admin(&mut context.pool(), person_id, report.community.id).await?; CommentReport::resolve(&mut context.pool(), report_id, person_id)
.await
if data.resolved { .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
CommentReport::resolve(&mut context.pool(), report_id, person_id) } else {
.await CommentReport::unresolve(&mut context.pool(), report_id, person_id)
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; .await
} else { .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
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,
})
} }
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::{ use lemmy_api_common::{
community::{BanFromCommunity, BanFromCommunityResponse}, community::{BanFromCommunity, BanFromCommunityResponse},
context::LemmyContext, 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::{ use lemmy_db_schema::{
source::{ source::{
@ -81,7 +86,7 @@ impl Perform for BanFromCommunity {
mod_person_id: local_user_view.person.id, mod_person_id: local_user_view.person.id,
other_person_id: data.person_id, other_person_id: data.person_id,
community_id: data.community_id, community_id: data.community_id,
reason: data.reason.clone(), reason: sanitize_html_opt(&data.reason),
banned: Some(data.ban), banned: Some(data.ban),
expires, expires,
}; };

View File

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

View File

@ -9,15 +9,15 @@ use lemmy_utils::{
}; };
use std::io::Cursor; use std::io::Cursor;
mod comment; pub mod comment;
mod comment_report; pub mod comment_report;
mod community; pub mod community;
mod local_user; pub mod local_user;
mod post; pub mod post;
mod post_report; pub mod post_report;
mod private_message; pub mod private_message;
mod private_message_report; pub mod private_message_report;
mod site; pub mod site;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait Perform { 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())) 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> { pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> {
let slur_regex = &local_site_to_slur_regex(local_site); 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::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{BanPerson, BanPersonResponse}, 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::{ use lemmy_db_schema::{
source::{ source::{
@ -63,7 +63,7 @@ impl Perform for BanPerson {
let form = ModBanForm { let form = ModBanForm {
mod_person_id: local_user_view.person.id, mod_person_id: local_user_view.person.id,
other_person_id: data.person_id, other_person_id: data.person_id,
reason: data.reason.clone(), reason: sanitize_html_opt(&data.reason),
banned: Some(data.ban), banned: Some(data.ban),
expires, expires,
}; };

View File

@ -5,15 +5,11 @@ use lemmy_api_common::{
person::{LoginResponse, PasswordChangeAfterReset}, person::{LoginResponse, PasswordChangeAfterReset},
utils::password_length_check, utils::password_length_check,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::source::{
source::{local_user::LocalUser, password_reset_request::PasswordResetRequest}, local_user::LocalUser,
RegistrationMode, password_reset_request::PasswordResetRequest,
};
use lemmy_db_views::structs::SiteView;
use lemmy_utils::{
claims::Claims,
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
}; };
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl Perform for PasswordChangeAfterReset { impl Perform for PasswordChangeAfterReset {
@ -38,30 +34,12 @@ impl Perform for PasswordChangeAfterReset {
// Update the user with the new password // Update the user with the new password
let password = data.password.clone(); let password = data.password.clone();
let updated_local_user = LocalUser::update_password(&mut context.pool(), local_user_id, &password)
LocalUser::update_password(&mut context.pool(), local_user_id, &password) .await
.await .with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
.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(),
)
};
Ok(LoginResponse { Ok(LoginResponse {
jwt, jwt: None,
verify_email_sent: false, verify_email_sent: false,
registration_created: false, registration_created: false,
}) })

View File

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

View File

@ -1,5 +1,4 @@
use crate::Perform; use actix_web::web::{Data, Json};
use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{CommentReplyResponse, MarkCommentReplyAsRead}, person::{CommentReplyResponse, MarkCommentReplyAsRead},
@ -12,41 +11,35 @@ use lemmy_db_schema::{
use lemmy_db_views_actor::structs::CommentReplyView; use lemmy_db_views_actor::structs::CommentReplyView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl Perform for MarkCommentReplyAsRead { pub async fn mark_reply_as_read(
type Response = CommentReplyResponse; 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))] let comment_reply_id = data.comment_reply_id;
async fn perform( let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
&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; if local_user_view.person.id != read_comment_reply.recipient_id {
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?; return Err(LemmyErrorType::CouldntUpdateComment)?;
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 })
} }
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; pub mod list_mentions;
mod list_replies; pub mod list_replies;
mod mark_all_read; pub mod mark_all_read;
mod mark_mention_read; pub mod mark_mention_read;
mod mark_reply_read; pub mod mark_reply_read;
mod unread_count; pub mod unread_count;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ impl Perform for ApproveRegistrationApplication {
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Update the registration with reason, admin_id // 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 { let app_form = RegistrationApplicationUpdateForm {
admin_id: Some(Some(local_user_view.person.id)), admin_id: Some(Some(local_user_view.person.id)),
deny_reason, deny_reason,

View File

@ -34,6 +34,7 @@ full = [
"actix-web", "actix-web",
"futures", "futures",
"once_cell", "once_cell",
"ammonia",
] ]
[dependencies] [dependencies]
@ -66,3 +67,4 @@ once_cell = { workspace = true, optional = true }
actix-web = { workspace = true, optional = true } actix-web = { workspace = true, optional = true }
# necessary for wasmt compilation # necessary for wasmt compilation
getrandom = { version = "0.2.10", features = ["js"] } 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}; use lemmy_utils::{error::LemmyError, utils::mention::MentionData};
pub async fn build_comment_response( pub async fn build_comment_response(
context: &Data<LemmyContext>, context: &LemmyContext,
comment_id: CommentId, comment_id: CommentId,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
form_id: Option<String>, form_id: Option<String>,

View File

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

View File

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

View File

@ -1,7 +1,7 @@
use crate::context::LemmyContext; use crate::context::LemmyContext;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use futures::future::BoxFuture; 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 lemmy_utils::{error::LemmyResult, SYNCHRONOUS_FEDERATION};
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::{Lazy, OnceCell};
use tokio::{ use tokio::{
@ -22,6 +22,7 @@ pub static MATCH_OUTGOING_ACTIVITIES: OnceCell<MatchOutgoingActivitiesBoxed> = O
#[derive(Debug)] #[derive(Debug)]
pub enum SendActivityData { pub enum SendActivityData {
CreatePost(Post), CreatePost(Post),
CreateComment(Comment),
} }
// TODO: instead of static, move this into LemmyContext. make sure that stopping the process with // 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 { pub struct ResolveObject {
/// Can be the full url, or a shortened version like: !fediverse@lemmy.ml /// Can be the full url, or a shortened version like: !fediverse@lemmy.ml
pub q: String, pub q: String,
pub auth: Sensitive<String>, pub auth: Option<Sensitive<String>>,
} }
#[skip_serializing_none] #[skip_serializing_none]

View File

@ -729,31 +729,6 @@ pub async fn delete_user_account(
Ok(()) 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 { pub enum EndpointType {
Community, Community,
Person, 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> { pub fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
Ok(Url::parse(&format!("{community_id}/moderators"))?.into()) 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 } tracing = { workspace = true }
url = { workspace = true } url = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
webmention = "0.4.0" webmention = "0.5.0"
chrono = { workspace = true } chrono = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }

View File

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

View File

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

View File

@ -1,5 +1,4 @@
use crate::PerformCrud; use actix_web::web::{Data, Json, Query};
use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
build_response::build_comment_response, build_response::build_comment_response,
comment::{CommentResponse, GetComment}, comment::{CommentResponse, GetComment},
@ -8,19 +7,19 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::source::local_site::LocalSite; use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
use std::ops::Deref;
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl PerformCrud for GetComment { pub async fn get_comment(
type Response = CommentResponse; 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))] check_private_instance(&local_user_view, &local_site)?;
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)?; Ok(Json(
build_comment_response(context.deref(), data.id, local_user_view, None, vec![]).await?,
build_comment_response(context, 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_db_views::structs::CommentView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
use std::ops::Deref;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl PerformCrud for RemoveComment { impl PerformCrud for RemoveComment {
@ -76,7 +77,7 @@ impl PerformCrud for RemoveComment {
.await?; .await?;
build_comment_response( build_comment_response(
context, context.deref(),
updated_comment.id, updated_comment.id,
Some(local_user_view), Some(local_user_view),
None, None,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
custom_emoji::{CreateCustomEmoji, CustomEmojiResponse}, 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::{ use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiInsertForm}, custom_emoji::{CustomEmoji, CustomEmojiInsertForm},
@ -26,11 +26,15 @@ impl PerformCrud for CreateCustomEmoji {
// Make sure user is an admin // Make sure user is an admin
is_admin(&local_user_view)?; 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() let emoji_form = CustomEmojiInsertForm::builder()
.local_site_id(local_site.id) .local_site_id(local_site.id)
.shortcode(data.shortcode.to_lowercase().trim().to_string()) .shortcode(shortcode)
.alt_text(data.alt_text.to_string()) .alt_text(alt_text)
.category(data.category.to_string()) .category(category)
.image_url(data.clone().image_url.into()) .image_url(data.clone().image_url.into())
.build(); .build();
let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?; 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::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
custom_emoji::{CustomEmojiResponse, EditCustomEmoji}, 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::{ use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiUpdateForm}, custom_emoji::{CustomEmoji, CustomEmojiUpdateForm},
@ -26,10 +26,13 @@ impl PerformCrud for EditCustomEmoji {
// Make sure user is an admin // Make sure user is an admin
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let alt_text = sanitize_html(&data.alt_text);
let category = sanitize_html(&data.category);
let emoji_form = CustomEmojiUpdateForm::builder() let emoji_form = CustomEmojiUpdateForm::builder()
.local_site_id(local_site.id) .local_site_id(local_site.id)
.alt_text(data.alt_text.to_string()) .alt_text(alt_text)
.category(data.category.to_string()) .category(category)
.image_url(data.clone().image_url.into()) .image_url(data.clone().image_url.into())
.build(); .build();
let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?; 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_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
mod comment; pub mod comment;
mod community; pub mod community;
mod custom_emoji; pub mod custom_emoji;
pub mod post; pub mod post;
mod private_message; pub mod private_message;
mod site; pub mod site;
mod user; pub mod user;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait PerformCrud { pub trait PerformCrud {

View File

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

View File

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

View File

@ -1,5 +1,4 @@
use crate::PerformCrud; use actix_web::web::{Data, Json, Query};
use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
post::{GetPost, GetPostResponse}, 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_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl PerformCrud for GetPost { pub async fn get_post(
type Response = GetPostResponse; 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))] check_private_instance(&local_user_view, &local_site)?;
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)?; 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 // Check to see if the person is a mod or admin, to show deleted / removed
let post_id = if let Some(id) = data.id { let community_id = Post::read(&mut context.pool(), post_id).await?.community_id;
id let is_mod_or_admin = is_mod_or_admin_opt(
} else if let Some(comment_id) = data.comment_id { &mut context.pool(),
Comment::read(&mut context.pool(), comment_id) local_user_view.as_ref(),
.await Some(community_id),
.with_lemmy_type(LemmyErrorType::CouldntFindPost)? )
.post_id .await
} else { .is_ok();
Err(LemmyErrorType::CouldntFindPost)?
};
// Check to see if the person is a mod or admin, to show deleted / removed let post_view = PostView::read(
let community_id = Post::read(&mut context.pool(), post_id).await?.community_id; &mut context.pool(),
let is_mod_or_admin = is_mod_or_admin_opt( post_id,
&mut context.pool(), person_id,
local_user_view.as_ref(), Some(is_mod_or_admin),
Some(community_id), )
) .await
.await .with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
.is_ok();
let post_view = PostView::read( // Mark the post as read
&mut context.pool(), let post_id = post_view.post.id;
post_id, if let Some(person_id) = person_id {
person_id, mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
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,
})
} }
// 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, context::LemmyContext,
post::{EditPost, PostResponse}, post::{EditPost, PostResponse},
request::fetch_site_data, 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::{ use lemmy_db_schema::{
source::{ source::{
@ -39,7 +44,6 @@ impl PerformCrud for EditPost {
// TODO No good way to handle a clear. // TODO No good way to handle a clear.
// Issue link: https://github.com/LemmyNet/lemmy/issues/2287 // Issue link: https://github.com/LemmyNet/lemmy/issues/2287
let url = Some(data_url.map(clean_url_params).map(Into::into)); 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); let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs_opt(&data.name, &slur_regex)?; 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))) .map(|u| (Some(u.title), Some(u.description), Some(u.embed_video_url)))
.unwrap_or_default(); .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; let language_id = self.language_id;
CommunityLanguage::is_allowed_community_language( CommunityLanguage::is_allowed_community_language(
&mut context.pool(), &mut context.pool(),
@ -84,7 +94,7 @@ impl PerformCrud for EditPost {
.await?; .await?;
let post_form = PostUpdateForm::builder() let post_form = PostUpdateForm::builder()
.name(data.name.clone()) .name(name)
.url(url) .url(url)
.body(body) .body(body)
.nsfw(data.nsfw) .nsfw(data.nsfw)

View File

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

View File

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

View File

@ -1,5 +1,4 @@
use crate::PerformCrud; use actix_web::web::{Data, Json, Query};
use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
private_message::{GetPrivateMessages, PrivateMessagesResponse}, private_message::{GetPrivateMessages, PrivateMessagesResponse},
@ -8,40 +7,34 @@ use lemmy_api_common::{
use lemmy_db_views::private_message_view::PrivateMessageQuery; use lemmy_db_views::private_message_view::PrivateMessageQuery;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl PerformCrud for GetPrivateMessages { pub async fn get_private_message(
type Response = PrivateMessagesResponse; 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))] let page = data.page;
async fn perform( let limit = data.limit;
&self, let unread_only = data.unread_only;
context: &Data<LemmyContext>, let mut messages = PrivateMessageQuery {
) -> Result<PrivateMessagesResponse, LemmyError> { page,
let data: &GetPrivateMessages = self; limit,
let local_user_view = local_user_view_from_jwt(data.auth.as_ref(), context).await?; unread_only,
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,
})
} }
.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::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
private_message::{EditPrivateMessage, PrivateMessageResponse}, 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::{ use lemmy_db_schema::{
source::{ source::{
@ -41,15 +41,16 @@ impl PerformCrud for EditPrivateMessage {
} }
// Doing the update // Doing the update
let content_slurs_removed = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site)); let content = sanitize_html(&data.content);
is_valid_body_field(&Some(content_slurs_removed.clone()), false)?; 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; let private_message_id = data.private_message_id;
PrivateMessage::update( PrivateMessage::update(
&mut context.pool(), &mut context.pool(),
private_message_id, private_message_id,
&PrivateMessageUpdateForm::builder() &PrivateMessageUpdateForm::builder()
.content(Some(content_slurs_removed)) .content(Some(content))
.updated(Some(Some(naive_now()))) .updated(Some(Some(naive_now())))
.build(), .build(),
) )

View File

@ -1,9 +1,6 @@
use crate::{ use crate::site::{application_question_check, site_default_post_listing_type_check};
site::{application_question_check, site_default_post_listing_type_check},
PerformCrud,
};
use activitypub_federation::http_signatures::generate_actor_keypair; use activitypub_federation::http_signatures::generate_actor_keypair;
use actix_web::web::Data; use actix_web::web::{Data, Json};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
site::{CreateSite, SiteResponse}, site::{CreateSite, SiteResponse},
@ -12,6 +9,8 @@ use lemmy_api_common::{
is_admin, is_admin,
local_site_rate_limit_to_rate_limit_config, local_site_rate_limit_to_rate_limit_config,
local_user_view_from_jwt, local_user_view_from_jwt,
sanitize_html,
sanitize_html_opt,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
@ -41,100 +40,105 @@ use lemmy_utils::{
}; };
use url::Url; use url::Url;
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl PerformCrud for CreateSite { pub async fn create_site(
type Response = SiteResponse; 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))] // Make sure user is an admin; other types of users should not create site data...
async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> { is_admin(&local_user_view)?;
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... validate_create_payload(&local_site, &data)?;
is_admin(&local_user_view)?;
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 site_form = SiteUpdateForm::builder()
let inbox_url = Some(generate_site_inbox_url(&actor_id)?); .name(Some(name))
let keypair = generate_actor_keypair()?; .sidebar(diesel_option_overwrite(sidebar))
let site_form = SiteUpdateForm::builder() .description(diesel_option_overwrite(description))
.name(Some(data.name.clone())) .icon(diesel_option_overwrite_to_url(&data.icon)?)
.sidebar(diesel_option_overwrite(&data.sidebar)) .banner(diesel_option_overwrite_to_url(&data.banner)?)
.description(diesel_option_overwrite(&data.description)) .actor_id(Some(actor_id))
.icon(diesel_option_overwrite_to_url(&data.icon)?) .last_refreshed_at(Some(naive_now()))
.banner(diesel_option_overwrite_to_url(&data.banner)?) .inbox_url(inbox_url)
.actor_id(Some(actor_id)) .private_key(Some(Some(keypair.private_key)))
.last_refreshed_at(Some(naive_now())) .public_key(Some(keypair.public_key))
.inbox_url(inbox_url) .build();
.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() let application_question = sanitize_html_opt(&data.application_question);
// Set the site setup to true let default_theme = sanitize_html_opt(&data.default_theme);
.site_setup(Some(true)) let legal_information = sanitize_html_opt(&data.legal_information);
.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();
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() LocalSite::update(&mut context.pool(), &local_site_form).await?;
.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?; 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 site_view = SiteView::read_local(&mut context.pool()).await?;
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
let rate_limit_config = let new_taglines = data.taglines.clone();
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
context
.settings_updated_channel()
.send(rate_limit_config)
.await?;
Ok(SiteResponse { let rate_limit_config =
site_view, local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
taglines, 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<()> { 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_db_schema::{ListingType, RegistrationMode};
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
mod create; pub mod create;
mod read; pub mod read;
mod update; pub mod update;
/// Checks whether the default post listing type is valid for a site. /// Checks whether the default post listing type is valid for a site.
pub fn site_default_post_listing_type_check( pub fn site_default_post_listing_type_check(

View File

@ -1,5 +1,4 @@
use crate::PerformCrud; use actix_web::web::{Data, Json, Query};
use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
sensitive::Sensitive, sensitive::Sensitive,
@ -28,76 +27,72 @@ use lemmy_utils::{
version, version,
}; };
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl PerformCrud for GetSite { pub async fn get_site(
type Response = GetSiteResponse; data: Query<GetSite>,
context: Data<LemmyContext>,
) -> Result<Json<GetSiteResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
#[tracing::instrument(skip(context))] let admins = PersonView::admins(&mut context.pool()).await?;
async fn perform(&self, context: &Data<LemmyContext>) -> Result<GetSiteResponse, LemmyError> {
let data: &GetSite = self;
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 person_id = local_user_view.person.id;
let my_user = if let Some(local_user_view) = let community_blocks = CommunityBlockView::for_person(&mut context.pool(), person_id)
local_user_settings_view_from_jwt_opt(data.auth.as_ref(), context).await .await
{ .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let person_id = local_user_view.person.id;
let local_user_id = local_user_view.local_user.id;
let follows = CommunityFollowerView::for_person(&mut context.pool(), person_id) let person_id = local_user_view.person.id;
.await let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id)
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?; .await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let person_id = local_user_view.person.id; let moderates = CommunityModeratorView::for_person(&mut context.pool(), person_id)
let community_blocks = CommunityBlockView::for_person(&mut context.pool(), person_id) .await
.await .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let person_id = local_user_view.person.id; let discussion_languages = LocalUserLanguage::read(&mut context.pool(), local_user_id)
let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id) .await
.await .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let moderates = CommunityModeratorView::for_person(&mut context.pool(), person_id) Some(MyUserInfo {
.await local_user_view,
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?; follows,
moderates,
let discussion_languages = LocalUserLanguage::read(&mut context.pool(), local_user_id) community_blocks,
.await person_blocks,
.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,
discussion_languages, 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)] #[tracing::instrument(skip_all)]

View File

@ -1,12 +1,14 @@
use crate::{ use crate::site::{application_question_check, site_default_post_listing_type_check};
site::{application_question_check, site_default_post_listing_type_check}, use actix_web::web::{Data, Json};
PerformCrud,
};
use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
site::{EditSite, SiteResponse}, 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::{ use lemmy_db_schema::{
source::{ source::{
@ -38,139 +40,142 @@ use lemmy_utils::{
}, },
}; };
#[async_trait::async_trait(?Send)] #[tracing::instrument(skip(context))]
impl PerformCrud for EditSite { pub async fn update_site(
type Response = SiteResponse; 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))] // Make sure user is an admin; other types of users should not update site data...
async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> { is_admin(&local_user_view)?;
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... validate_update_payload(&local_site, &data)?;
is_admin(&local_user_view)?;
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?;
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)
} }
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<()> { fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> LemmyResult<()> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ use activitypub_federation::{
}; };
use lemmy_api_common::{ use lemmy_api_common::{
build_response::send_local_notifs, build_response::send_local_notifs,
comment::{CommentResponse, CreateComment, EditComment}, comment::{CommentResponse, EditComment},
context::LemmyContext, context::LemmyContext,
utils::{check_post_deleted_or_removed, is_mod_or_admin}, 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 lemmy_utils::{error::LemmyError, utils::mention::scrape_text_for_mentions};
use url::Url; 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] #[async_trait::async_trait]
impl SendActivity for EditComment { impl SendActivity for EditComment {
type Response = CommentResponse; type Response = CommentResponse;
@ -73,10 +54,10 @@ impl SendActivity for EditComment {
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
CreateOrUpdateNote::send( CreateOrUpdateNote::send(
&response.comment_view.comment, response.comment_view.comment.clone(),
response.comment_view.creator.id, response.comment_view.creator.id,
CreateOrUpdateType::Update, CreateOrUpdateType::Update,
context, context.reset_request_count(),
) )
.await .await
} }
@ -84,11 +65,11 @@ impl SendActivity for EditComment {
impl CreateOrUpdateNote { impl CreateOrUpdateNote {
#[tracing::instrument(skip(comment, person_id, kind, context))] #[tracing::instrument(skip(comment, person_id, kind, context))]
async fn send( pub(crate) async fn send(
comment: &Comment, comment: Comment,
person_id: PersonId, person_id: PersonId,
kind: CreateOrUpdateType, kind: CreateOrUpdateType,
context: &Data<LemmyContext>, context: Data<LemmyContext>,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
// TODO: might be helpful to add a comment method to retrieve community directly // TODO: might be helpful to add a comment method to retrieve community directly
let post_id = comment.post_id; let post_id = comment.post_id;
@ -103,7 +84,7 @@ impl CreateOrUpdateNote {
kind.clone(), kind.clone(),
&context.settings().get_protocol_and_hostname(), &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 { let create_or_update = CreateOrUpdateNote {
actor: person.id().into(), actor: person.id().into(),
@ -131,12 +112,12 @@ impl CreateOrUpdateNote {
.collect(); .collect();
let mut inboxes = ActivitySendTargets::empty(); let mut inboxes = ActivitySendTargets::empty();
for t in tagged_users { 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()); inboxes.add_inbox(person.shared_inbox_or_inbox());
} }
let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update); 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}, protocol::{activities::deletion::delete::Delete, IdOrNestedObject},
}; };
use activitypub_federation::{config::Data, kinds::activity::DeleteType, traits::ActivityHandler}; 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::{ use lemmy_db_schema::{
source::{ source::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
@ -105,6 +105,8 @@ pub(in crate::activities) async fn receive_remove_action(
reason: Option<String>, reason: Option<String>,
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let reason = sanitize_html_opt(&reason);
match DeletableObjects::read_from_db(object, context).await? { match DeletableObjects::read_from_db(object, context).await? {
DeletableObjects::Community(community) => { DeletableObjects::Community(community) => {
if community.local { if community.local {

View File

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

View File

@ -24,24 +24,32 @@ use crate::{
InCommunity, InCommunity,
}, },
}; };
use activitypub_federation::{ use activitypub_federation::{config::Data, traits::ActivityHandler};
config::Data,
protocol::context::WithContext,
traits::ActivityHandler,
};
use lemmy_api_common::context::LemmyContext; use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; 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)] #[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)] #[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)] #[enum_delegate::implement(ActivityHandler)]
pub enum SharedInboxActivities { pub enum SharedInboxActivities {
PersonInboxActivities(Box<WithContext<PersonInboxActivities>>), Follow(Follow),
GroupInboxActivities(Box<WithContext<GroupInboxActivities>>), 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)] #[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)] #[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)] #[enum_delegate::implement(ActivityHandler)]
@ -49,10 +57,11 @@ pub enum GroupInboxActivities {
Follow(Follow), Follow(Follow),
UndoFollow(UndoFollow), UndoFollow(UndoFollow),
Report(Report), Report(Report),
// This is a catch-all and needs to be last /// This is a catch-all and needs to be last
AnnouncableActivities(RawAnnouncableActivities), AnnouncableActivities(RawAnnouncableActivities),
} }
/// List of activities which the person inbox can handle.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)] #[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)] #[enum_delegate::implement(ActivityHandler)]
@ -64,17 +73,8 @@ pub enum PersonInboxActivities {
Delete(Delete), Delete(Delete),
UndoDelete(UndoDelete), UndoDelete(UndoDelete),
AnnounceActivity(AnnounceActivity), AnnounceActivity(AnnounceActivity),
} /// User can also receive some "announcable" activities, eg a comment mention.
AnnouncableActivities(AnnouncableActivities),
/// 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>),
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
@ -138,12 +138,7 @@ mod tests {
#![allow(clippy::indexing_slicing)] #![allow(clippy::indexing_slicing)]
use crate::{ use crate::{
activity_lists::{ activity_lists::{GroupInboxActivities, PersonInboxActivities, SiteInboxActivities},
GroupInboxActivities,
PersonInboxActivities,
PersonInboxActivitiesWithAnnouncable,
SiteInboxActivities,
},
protocol::tests::{test_json, test_parse_lemmy_item}, protocol::tests::{test_json, test_parse_lemmy_item},
}; };
@ -161,16 +156,15 @@ mod tests {
fn test_person_inbox() { fn test_person_inbox() {
test_parse_lemmy_item::<PersonInboxActivities>("assets/lemmy/activities/following/accept.json") test_parse_lemmy_item::<PersonInboxActivities>("assets/lemmy/activities/following/accept.json")
.unwrap(); .unwrap();
test_parse_lemmy_item::<PersonInboxActivitiesWithAnnouncable>( test_parse_lemmy_item::<PersonInboxActivities>(
"assets/lemmy/activities/create_or_update/create_note.json", "assets/lemmy/activities/create_or_update/create_note.json",
) )
.unwrap(); .unwrap();
test_parse_lemmy_item::<PersonInboxActivitiesWithAnnouncable>( test_parse_lemmy_item::<PersonInboxActivities>(
"assets/lemmy/activities/create_or_update/create_private_message.json", "assets/lemmy/activities/create_or_update/create_private_message.json",
) )
.unwrap(); .unwrap();
test_json::<PersonInboxActivitiesWithAnnouncable>("assets/mastodon/activities/follow.json") test_json::<PersonInboxActivities>("assets/mastodon/activities/follow.json").unwrap();
.unwrap();
} }
#[test] #[test]

View File

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

View File

@ -16,7 +16,7 @@ use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn read_community( pub async fn get_community(
data: Query<GetCommunity>, data: Query<GetCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> Result<Json<GetCommunityResponse>, LemmyError> { ) -> 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 activitypub_federation::config::Data;
use actix_web::web::{Json, Query}; use actix_web::web::{Json, Query};
use diesel::NotFound; use diesel::NotFound;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
site::{ResolveObject, ResolveObjectResponse}, 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_schema::{newtypes::PersonId, source::local_site::LocalSite, utils::DbPool};
use lemmy_db_views::structs::{CommentView, PostView}; use lemmy_db_views::structs::{CommentView, PostView};
@ -17,14 +21,23 @@ pub async fn resolve_object(
data: Query<ResolveObject>, data: Query<ResolveObject>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> Result<Json<ResolveObjectResponse>, LemmyError> { ) -> 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 local_site = LocalSite::read(&mut context.pool()).await?;
let person_id = local_user_view.person.id; check_private_instance(&local_user_view, &local_site)?;
check_private_instance(&Some(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()) convert_response(res, person_id, &mut context.pool())
.await .await
.with_lemmy_type(LemmyErrorType::CouldntFindObject) .with_lemmy_type(LemmyErrorType::CouldntFindObject)
@ -32,7 +45,7 @@ pub async fn resolve_object(
async fn convert_response( async fn convert_response(
object: SearchableObjects, object: SearchableObjects,
user_id: PersonId, user_id: Option<PersonId>,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> Result<Json<ResolveObjectResponse>, LemmyError> { ) -> Result<Json<ResolveObjectResponse>, LemmyError> {
use SearchableObjects::*; use SearchableObjects::*;
@ -45,15 +58,15 @@ async fn convert_response(
} }
Community(c) => { Community(c) => {
removed_or_deleted = c.deleted || c.removed; 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) => { Post(p) => {
removed_or_deleted = p.deleted || p.removed; 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) => { Comment(c) => {
removed_or_deleted = c.deleted || c.removed; 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 // if the object was deleted from database, dont return it

View File

@ -8,7 +8,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{community::Community, local_site::LocalSite}, source::{community::Community, local_site::LocalSite},
utils::post_to_comment_sort_type, utils::{post_to_comment_sort_type, post_to_person_sort_type},
SearchType, SearchType,
}; };
use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery}; use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery};
@ -98,7 +98,7 @@ pub async fn search(
} }
SearchType::Users => { SearchType::Users => {
users = PersonQuery { users = PersonQuery {
sort: (sort), sort: (sort.map(post_to_person_sort_type)),
search_term: (Some(q)), search_term: (Some(q)),
page: (page), page: (page),
limit: (limit), limit: (limit),
@ -168,7 +168,7 @@ pub async fn search(
vec![] vec![]
} else { } else {
PersonQuery { PersonQuery {
sort: (sort), sort: (sort.map(post_to_person_sort_type)),
search_term: (Some(q)), search_term: (Some(q)),
page: (page), page: (page),
limit: (limit), 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. /// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum SearchableObjects { pub(crate) enum SearchableObjects {

View File

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

View File

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

View File

@ -16,7 +16,10 @@ use activitypub_federation::{
traits::Object, traits::Object,
}; };
use chrono::NaiveDateTime; 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::{ use lemmy_db_schema::{
source::{ source::{
comment::{Comment, CommentInsertForm, CommentUpdateForm}, comment::{Comment, CommentInsertForm, CommentUpdateForm},
@ -154,14 +157,15 @@ impl Object for ApubComment {
let local_site = LocalSite::read(&mut context.pool()).await.ok(); let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site); 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 = let language_id =
LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?; LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?;
let form = CommentInsertForm { let form = CommentInsertForm {
creator_id: creator.id, creator_id: creator.id,
post_id: post.id, post_id: post.id,
content: content_slurs_removed, content,
removed: None, removed: None,
published: note.published.map(|u| u.naive_local()), published: note.published.map(|u| u.naive_local()),
updated: note.updated.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}, traits::{Actor, Object},
}; };
use chrono::NaiveDateTime; 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::{ use lemmy_db_schema::{
newtypes::InstanceId, newtypes::InstanceId,
source::{ source::{
@ -131,13 +134,17 @@ impl Object for ApubSite {
let domain = apub.id.inner().domain().expect("group id has domain"); 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 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 { let site_form = SiteInsertForm {
name: apub.name.clone(), 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()), updated: apub.updated.map(|u| u.clone().naive_local()),
icon: apub.icon.clone().map(|i| i.url.into()), icon: apub.icon.clone().map(|i| i.url.into()),
banner: apub.image.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()), actor_id: Some(apub.id.clone().into()),
last_refreshed_at: Some(naive_now()), last_refreshed_at: Some(naive_now()),
inbox_url: Some(apub.inbox.clone().into()), inbox_url: Some(apub.inbox.clone().into()),

View File

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

View File

@ -25,7 +25,13 @@ use html2md::parse_html;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
request::fetch_site_data, 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::{ use lemmy_db_schema::{
self, self,
@ -228,6 +234,10 @@ impl Object for ApubPost {
let language_id = let language_id =
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?; 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 { PostInsertForm {
name, name,
url: url.map(Into::into), url: url.map(Into::into),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,11 @@ pub mod newtypes;
#[rustfmt::skip] #[rustfmt::skip]
#[allow(clippy::wildcard_imports)] #[allow(clippy::wildcard_imports)]
pub mod schema; 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; pub mod source;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod traits; pub mod traits;
@ -66,6 +71,7 @@ pub enum SortType {
TopThreeMonths, TopThreeMonths,
TopSixMonths, TopSixMonths,
TopNineMonths, TopNineMonths,
Controversial,
} }
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)] #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)]
@ -77,6 +83,20 @@ pub enum CommentSortType {
Top, Top,
New, New,
Old, 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)] #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]

View File

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

View File

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

View File

@ -2,7 +2,9 @@ use crate::{
diesel::Connection, diesel::Connection,
diesel_migrations::MigrationHarness, diesel_migrations::MigrationHarness,
newtypes::DbUrl, newtypes::DbUrl,
traits::JoinView,
CommentSortType, CommentSortType,
PersonSortType,
SortType, SortType,
}; };
use activitypub_federation::{fetch::object_id::ObjectId, traits::Object}; use activitypub_federation::{fetch::object_id::ObjectId, traits::Object};
@ -25,7 +27,7 @@ use diesel_async::{
}, },
}; };
use diesel_migrations::EmbeddedMigrations; use diesel_migrations::EmbeddedMigrations;
use futures_util::{future::BoxFuture, FutureExt}; use futures_util::{future::BoxFuture, Future, FutureExt};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType}, error::{LemmyError, LemmyErrorExt, LemmyErrorType},
settings::structs::Settings, settings::structs::Settings,
@ -196,12 +198,12 @@ pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test) 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 { match opt {
// An empty string is an erase // An empty string is an erase
Some(unwrapped) => { Some(unwrapped) => {
if !unwrapped.eq("") { if !unwrapped.eq("") {
Some(Some(unwrapped.clone())) Some(Some(unwrapped))
} else { } else {
Some(None) Some(None)
} }
@ -347,6 +349,7 @@ pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType {
SortType::Active | SortType::Hot => CommentSortType::Hot, SortType::Active | SortType::Hot => CommentSortType::Hot,
SortType::New | SortType::NewComments | SortType::MostComments => CommentSortType::New, SortType::New | SortType::NewComments | SortType::MostComments => CommentSortType::New,
SortType::Old => CommentSortType::Old, SortType::Old => CommentSortType::Old,
SortType::Controversial => CommentSortType::Controversial,
SortType::TopHour SortType::TopHour
| SortType::TopSixHour | SortType::TopSixHour
| SortType::TopTwelveHour | 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(|| { static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$") Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
.expect("compile email regex") .expect("compile email regex")
@ -373,6 +386,10 @@ pub mod functions {
fn hot_rank(score: BigInt, time: Timestamp) -> Integer; 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); sql_function!(fn lower(x: Text) -> Text);
// really this function is variadic, this just adds the two-argument version // 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)] #[cfg(test)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
@ -432,10 +537,10 @@ mod tests {
#[test] #[test]
fn test_diesel_option_overwrite() { fn test_diesel_option_overwrite() {
assert_eq!(diesel_option_overwrite(&None), None); assert_eq!(diesel_option_overwrite(None), None);
assert_eq!(diesel_option_overwrite(&Some(String::new())), Some(None)); assert_eq!(diesel_option_overwrite(Some(String::new())), Some(None));
assert_eq!( assert_eq!(
diesel_option_overwrite(&Some("test".to_string())), diesel_option_overwrite(Some("test".to_string())),
Some(Some("test".to_string())) Some(Some("test".to_string()))
); );
} }

View File

@ -1,6 +1,7 @@
use crate::structs::CommentReportView; use crate::structs::CommentReportView;
use diesel::{ use diesel::{
dsl::now, dsl::now,
pg::Pg,
result::Error, result::Error,
BoolExpressionMethods, BoolExpressionMethods,
ExpressionMethods, ExpressionMethods,
@ -11,6 +12,7 @@ use diesel::{
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::CommentAggregates, aggregates::structs::CommentAggregates,
aliases,
newtypes::{CommentReportId, CommunityId, PersonId}, newtypes::{CommentReportId, CommunityId, PersonId},
schema::{ schema::{
comment, comment,
@ -31,9 +33,119 @@ use lemmy_db_schema::{
post::Post, post::Post,
}, },
traits::JoinView, 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 { impl CommentReportView {
/// returns the CommentReportView for the provided report_id /// returns the CommentReportView for the provided report_id
/// ///
@ -43,54 +155,7 @@ impl CommentReportView {
report_id: CommentReportId, report_id: CommentReportId,
my_person_id: PersonId, my_person_id: PersonId,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?; queries().read(pool, (report_id, my_person_id)).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))
} }
/// Returns the current unresolved post report count for the communities you mod /// Returns the current unresolved post report count for the communities you mod
@ -150,90 +215,7 @@ impl CommentReportQuery {
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
my_person: &Person, my_person: &Person,
) -> Result<Vec<CommentReportView>, Error> { ) -> Result<Vec<CommentReportView>, Error> {
let conn = &mut get_conn(pool).await?; queries().list(pool, (self, my_person)).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())
} }
} }
@ -478,6 +460,7 @@ mod tests {
published: agg.published, published: agg.published,
child_count: 0, child_count: 0,
hot_rank: 1728, hot_rank: 1728,
controversy_rank: 0.0,
}, },
my_vote: None, my_vote: None,
resolver: None, resolver: None,

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