Merge remote-tracking branch 'origin/main' into post-tags

This commit is contained in:
phiresky 2024-11-30 15:26:39 +01:00
commit 37246e96d6
202 changed files with 5470 additions and 3370 deletions

View file

@ -91,6 +91,36 @@ steps:
when:
- event: pull_request
cargo_clippy:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- rustup component add clippy
- cargo clippy --workspace --tests --all-targets -- -D warnings
when: *slow_check_paths
cargo_test:
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
LEMMY_TEST_FAST_FEDERATION: "1"
LEMMY_CONFIG_LOCATION: ../../config/config.hjson
commands:
- cargo test --workspace --no-fail-fast
when: *slow_check_paths
check_ts_bindings:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- ./scripts/ts_bindings_check.sh
when:
- event: pull_request
# make sure api builds with default features (used by other crates relying on lemmy api)
check_api_common_default_features:
image: *rust_image
@ -138,26 +168,6 @@ steps:
- diff tmp.schema crates/db_schema/src/schema.rs
when: *slow_check_paths
check_db_perf_tool:
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
commands:
# same as scripts/db_perf.sh but without creating a new database server
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
when: *slow_check_paths
cargo_clippy:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- rustup component add clippy
- cargo clippy --workspace --tests --all-targets -- -D warnings
when: *slow_check_paths
cargo_build:
image: *rust_image
environment:
@ -167,27 +177,6 @@ steps:
- mv target/debug/lemmy_server target/lemmy_server
when: *slow_check_paths
cargo_test:
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
LEMMY_TEST_FAST_FEDERATION: "1"
LEMMY_CONFIG_LOCATION: ../../config/config.hjson
commands:
- cargo test --workspace --no-fail-fast
when: *slow_check_paths
check_ts_bindings:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- ./scripts/ts_bindings_check.sh
when:
- event: pull_request
check_diesel_migration:
# TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server
image: *rust_image
@ -221,6 +210,17 @@ steps:
- diff before.sqldump after.sqldump
when: *slow_check_paths
check_db_perf_tool:
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
commands:
# same as scripts/db_perf.sh but without creating a new database server
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
when: *slow_check_paths
run_federation_tests:
image: node:22-bookworm-slim
environment:
@ -280,24 +280,26 @@ steps:
# using https://github.com/pksunkara/cargo-workspaces
publish_to_crates_io:
image: *rust_image
environment:
CARGO_API_TOKEN:
from_secret: cargo_api_token
commands:
- *install_binstall
# Install cargo-workspaces
- cargo binstall -y cargo-workspaces
- cp -r migrations crates/db_schema/
- cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
secrets: [cargo_api_token]
when:
- event: tag
notify_on_failure:
notify_on_build:
image: alpine:3
commands:
- apk add curl
- "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
- "curl -d'Lemmy CI build ${CI_PIPELINE_STATUS}: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
when:
- event: [pull_request, tag]
status: failure
status: [failure, success]
notify_on_tag_deploy:
image: alpine:3

341
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "accept-language"
@ -10,9 +10,9 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772"
[[package]]
name = "activitypub_federation"
version = "0.6.0-alpha2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4877d467ddf2fac85e9ee33aba6f2560df14125b8bfa864f85ab40e9b87753a9"
checksum = "ee819cada736b6e26c59706f9e6ff89a48060e635c0546ff984d84baefc8c13a"
dependencies = [
"activitystreams-kinds",
"actix-web",
@ -137,7 +137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -272,7 +272,7 @@ dependencies = [
"actix-router",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -435,9 +435,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
dependencies = [
"backtrace",
]
@ -484,13 +484,13 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.82"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -635,7 +635,7 @@ dependencies = [
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn 2.0.77",
"syn 2.0.87",
"which",
]
@ -833,9 +833,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.19"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
dependencies = [
"clap_builder",
"clap_derive",
@ -843,9 +843,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.19"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
dependencies = [
"anstream",
"anstyle",
@ -862,7 +862,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1096,7 +1096,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.11.1",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1118,20 +1118,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core 0.20.10",
"quote",
"syn 2.0.77",
]
[[package]]
name = "deadpool"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e"
dependencies = [
"async-trait",
"deadpool-runtime",
"num_cpus",
"retain_mut",
"tokio",
"syn 2.0.87",
]
[[package]]
@ -1194,7 +1181,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1215,7 +1202,7 @@ dependencies = [
"darling 0.20.10",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1225,7 +1212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc"
dependencies = [
"derive_builder_core",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1238,7 +1225,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1252,9 +1239,9 @@ dependencies = [
[[package]]
name = "diesel"
version = "2.1.6"
version = "2.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2"
checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e"
dependencies = [
"bitflags 2.6.0",
"byteorder",
@ -1268,12 +1255,12 @@ dependencies = [
[[package]]
name = "diesel-async"
version = "0.4.1"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acada1517534c92d3f382217b485db8a8638f111b0e3f2a2a8e26165050f77be"
checksum = "4c5c6ec8d5c7b8444d19a47161797cbe361e0fb1ee40c6a8124ec915b64a4125"
dependencies = [
"async-trait",
"deadpool 0.9.5",
"deadpool",
"diesel",
"futures-util",
"scoped-futures",
@ -1281,6 +1268,15 @@ dependencies = [
"tokio-postgres",
]
[[package]]
name = "diesel-bind-if-some"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ed8ce9db476124d2eaf4c9db45dc6581b8e8c4c4d47d5e0f39de1fb55dfb2a7"
dependencies = [
"diesel",
]
[[package]]
name = "diesel-derive-enum"
version = "2.1.0"
@ -1290,7 +1286,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1301,19 +1297,20 @@ checksum = "d5adf688c584fe33726ce0e2898f608a2a92578ac94a4a92fcecf73214fe0716"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
name = "diesel_derives"
version = "2.1.4"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14701062d6bed917b5c7103bdffaee1e4609279e240488ad24e7bd979ca6866c"
checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4"
dependencies = [
"diesel_table_macro_syntax",
"dsl_auto_type",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1328,9 +1325,9 @@ dependencies = [
[[package]]
name = "diesel_migrations"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac"
checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6"
dependencies = [
"diesel",
"migrations_internals",
@ -1339,11 +1336,11 @@ dependencies = [
[[package]]
name = "diesel_table_macro_syntax"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25"
dependencies = [
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1381,7 +1378,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1420,6 +1417,20 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dsl_auto_type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607"
dependencies = [
"darling 0.20.10",
"either",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "dunce"
version = "1.0.5"
@ -1465,9 +1476,9 @@ checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
[[package]]
name = "encoding_rs"
version = "0.8.34"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
@ -1495,7 +1506,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1693,7 +1704,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -1905,7 +1916,7 @@ dependencies = [
"markup5ever 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -2069,7 +2080,7 @@ dependencies = [
"http 1.1.0",
"hyper 1.4.1",
"hyper-util",
"rustls 0.23.14",
"rustls 0.23.16",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.0",
@ -2114,7 +2125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8215279f83f9b829403812f845aa2d0dd5966332aa2fd0334a375256f3dd0322"
dependencies = [
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -2255,7 +2266,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -2276,24 +2287,23 @@ dependencies = [
[[package]]
name = "idna"
version = "0.5.0"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna"
version = "1.0.2"
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
"smallvec",
"utf8_iter",
]
[[package]]
@ -2497,7 +2507,6 @@ dependencies = [
"encoding_rs",
"enum-map",
"futures",
"getrandom",
"jsonwebtoken",
"lemmy_db_schema",
"lemmy_db_views",
@ -2505,6 +2514,7 @@ dependencies = [
"lemmy_db_views_moderator",
"lemmy_utils",
"mime",
"mime_guess",
"moka",
"pretty_assertions",
"regex",
@ -2539,7 +2549,6 @@ dependencies = [
"lemmy_db_views",
"lemmy_db_views_actor",
"lemmy_utils",
"moka",
"serde",
"serde_json",
"serde_with",
@ -2609,10 +2618,11 @@ dependencies = [
"async-trait",
"bcrypt",
"chrono",
"deadpool 0.12.1",
"deadpool",
"derive-new",
"diesel",
"diesel-async",
"diesel-bind-if-some",
"diesel-derive-enum",
"diesel-derive-newtype",
"diesel_ltree",
@ -2620,10 +2630,9 @@ dependencies = [
"futures-util",
"i-love-jesus",
"lemmy_utils",
"moka",
"pretty_assertions",
"regex",
"rustls 0.23.14",
"rustls 0.23.16",
"serde",
"serde_json",
"serde_with",
@ -2634,6 +2643,7 @@ dependencies = [
"tokio-postgres-rustls",
"tracing",
"ts-rs",
"tuplex",
"url",
"uuid",
]
@ -2775,7 +2785,7 @@ dependencies = [
"reqwest 0.12.8",
"reqwest-middleware",
"reqwest-tracing",
"rustls 0.23.14",
"rustls 0.23.16",
"serde_json",
"serial_test",
"tokio",
@ -2807,6 +2817,7 @@ dependencies = [
"markdown-it-ruby",
"markdown-it-sub",
"markdown-it-sup",
"moka",
"pretty_assertions",
"regex",
"reqwest-middleware",
@ -2826,9 +2837,9 @@ dependencies = [
[[package]]
name = "lettre"
version = "0.11.9"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f204773bab09b150320ea1c83db41dc6ee606a4bc36dc1f43005fe7b58ce06"
checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9"
dependencies = [
"async-trait",
"base64 0.22.1",
@ -2839,12 +2850,12 @@ dependencies = [
"futures-io",
"futures-util",
"httpdate",
"idna 1.0.2",
"idna 1.0.3",
"mime",
"nom",
"percent-encoding",
"quoted_printable",
"rustls 0.23.14",
"rustls 0.23.16",
"rustls-pemfile 2.1.3",
"rustls-pki-types",
"socket2",
@ -3111,9 +3122,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "migrations_internals"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada"
checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff"
dependencies = [
"serde",
"toml",
@ -3121,9 +3132,9 @@ dependencies = [
[[package]]
name = "migrations_macros"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08"
checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd"
dependencies = [
"migrations_internals",
"proc-macro2",
@ -3136,6 +3147,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -3203,7 +3224,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -3515,7 +3536,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -3685,7 +3706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
dependencies = [
"proc-macro2",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -3786,6 +3807,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.5"
@ -3797,7 +3828,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.0.0",
"rustls 0.23.14",
"rustls 0.23.16",
"socket2",
"thiserror",
"tokio",
@ -3814,7 +3845,7 @@ dependencies = [
"rand",
"ring",
"rustc-hash 2.0.0",
"rustls 0.23.14",
"rustls 0.23.16",
"slab",
"thiserror",
"tinyvec",
@ -3896,7 +3927,7 @@ checksum = "a25d631e41bfb5fdcde1d4e2215f62f7f0afa3ff11e26563765bd6ea1d229aeb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -3910,9 +3941,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@ -4025,7 +4056,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.14",
"rustls 0.23.16",
"rustls-pemfile 2.1.3",
"rustls-pki-types",
"serde",
@ -4076,12 +4107,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "retain_mut"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0"
[[package]]
name = "rgb"
version = "0.8.50"
@ -4148,14 +4173,14 @@ dependencies = [
[[package]]
name = "rss"
version = "2.0.9"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27e92048f840d98c6d6dd870af9101610ea9ff413f11f1bcebf4f4c31d96d957"
checksum = "554a62b3dd5450fcbb0435b3db809f9dd3c6e9f5726172408f7ad3b57ed59057"
dependencies = [
"atom_syndication",
"derive_builder",
"never",
"quick-xml 0.36.1",
"quick-xml 0.37.1",
]
[[package]]
@ -4212,9 +4237,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.14"
version = "0.23.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
dependencies = [
"aws-lc-rs",
"log",
@ -4247,9 +4272,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
[[package]]
name = "rustls-webpki"
@ -4354,29 +4379,29 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"indexmap 2.5.0",
"itoa",
@ -4433,14 +4458,14 @@ dependencies = [
"darling 0.20.10",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
name = "serial_test"
version = "3.1.1"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d"
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
dependencies = [
"futures",
"log",
@ -4452,13 +4477,13 @@ dependencies = [
[[package]]
name = "serial_test_derive"
version = "3.1.1"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67"
checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -4543,9 +4568,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "sitemap-rs"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88cc73a9aac975541c9054e74ceae8d8ee85edc89a322404c275c1d100fffa51"
checksum = "3c4c6ab96128064ba085256d34e205153555b3803094d76e24d406c76f85a2c9"
dependencies = [
"chrono",
"xml-builder",
@ -4574,7 +4599,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -4702,7 +4727,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -4724,9 +4749,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.77"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
@ -4756,7 +4781,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -4852,7 +4877,7 @@ checksum = "78ea17a2dc368aeca6f554343ced1b1e31f76d63683fa8016e5844bd7a5144a1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -4872,7 +4897,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -4949,9 +4974,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.40.0"
version = "1.41.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
dependencies = [
"backtrace",
"bytes",
@ -4973,7 +4998,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -5009,7 +5034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab"
dependencies = [
"ring",
"rustls 0.23.14",
"rustls 0.23.16",
"tokio",
"tokio-postgres",
"tokio-rustls 0.26.0",
@ -5032,7 +5057,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.14",
"rustls 0.23.16",
"rustls-pki-types",
"tokio",
]
@ -5052,9 +5077,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.8"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
@ -5073,9 +5098,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.15"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.5.0",
"serde",
@ -5160,7 +5185,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -5233,7 +5258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568"
dependencies = [
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -5269,16 +5294,28 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
"termcolor",
]
[[package]]
name = "tuplex"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "676ac81d5454c4dcf37955d34fa8626ede3490f744b86ca14a7b90168d2a08aa"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicase"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
@ -5332,12 +5369,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.2"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
dependencies = [
"form_urlencoded",
"idna 0.5.0",
"idna 1.0.3",
"percent-encoding",
"serde",
]
@ -5380,9 +5417,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"serde",
@ -5459,7 +5496,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
"wasm-bindgen-shared",
]
@ -5493,7 +5530,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -5813,9 +5850,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.5.40"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
@ -5924,7 +5961,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
"synstructure",
]
@ -5946,7 +5983,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -5966,7 +6003,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
"synstructure",
]
@ -5987,7 +6024,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]
@ -6009,7 +6046,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"syn 2.0.87",
]
[[package]]

View file

@ -79,6 +79,8 @@ unused_self = "deny"
unwrap_used = "deny"
unimplemented = "deny"
unused_async = "deny"
map_err_ignore = "deny"
expect_used = "deny"
[workspace.dependencies]
lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" }
@ -92,13 +94,13 @@ lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" }
lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" }
activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [
activitypub_federation = { version = "0.6.1", default-features = false, features = [
"actix-web",
] }
diesel = "2.1.6"
diesel_migrations = "2.1.0"
diesel-async = "0.4.1"
serde = { version = "1.0.204", features = ["derive"] }
diesel = "2.2.4"
diesel_migrations = "2.2.0"
diesel-async = "0.5.1"
serde = { version = "1.0.215", features = ["derive"] }
serde_with = "3.9.0"
actix-web = { version = "4.9.0", default-features = false, features = [
"macros",
@ -111,7 +113,7 @@ actix-web = { version = "4.9.0", default-features = false, features = [
tracing = "0.1.40"
tracing-actix-web = { version = "0.7.10", default-features = false }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.2", features = ["serde"] }
url = { version = "2.5.3", features = ["serde"] }
reqwest = { version = "0.12.7", default-features = false, features = [
"json",
"blocking",
@ -123,24 +125,27 @@ reqwest-tracing = "0.5.3"
clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.1"
chrono = { version = "0.4.38", features = ["serde"], default-features = false }
serde_json = { version = "1.0.121", features = ["preserve_order"] }
chrono = { version = "0.4.38", features = [
"serde",
"now",
], default-features = false }
serde_json = { version = "1.0.132", features = ["preserve_order"] }
base64 = "0.22.1"
uuid = { version = "1.10.0", features = ["serde", "v4"] }
async-trait = "0.1.81"
uuid = { version = "1.11.0", features = ["serde"] }
async-trait = "0.1.83"
captcha = "0.0.9"
anyhow = { version = "1.0.86", features = [
anyhow = { version = "1.0.93", features = [
"backtrace",
] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.1"
serial_test = "3.1.1"
tokio = { version = "1.39.2", features = ["full"] }
regex = "1.10.5"
serial_test = "3.2.0"
tokio = { version = "1.41.1", features = ["full"] }
regex = "1.11.1"
diesel-derive-newtype = "2.1.2"
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = { version = "0.26.3", features = ["derive"] }
itertools = "0.13.0"
futures = "0.3.30"
futures = "0.3.31"
http = "1.1"
rosetta-i18n = "0.1.3"
ts-rs = { version = "10.0.0", features = [
@ -149,17 +154,19 @@ ts-rs = { version = "10.0.0", features = [
"no-serde-warnings",
"url-impl",
] }
rustls = { version = "0.23.12", features = ["ring"] }
futures-util = "0.3.30"
tokio-postgres = "0.7.11"
rustls = { version = "0.23.16", features = ["ring"] }
futures-util = "0.3.31"
tokio-postgres = "0.7.12"
tokio-postgres-rustls = "0.12.0"
urlencoding = "2.1.3"
enum-map = "2.7"
moka = { version = "0.12.8", features = ["future"] }
i-love-jesus = { version = "0.1.0" }
clap = { version = "4.5.13", features = ["derive", "env"] }
pretty_assertions = "1.4.0"
clap = { version = "4.5.21", features = ["derive", "env"] }
pretty_assertions = "1.4.1"
derive-new = "0.7.0"
diesel-bind-if-some = "0.1.0"
tuplex = "0.1.2"
[dependencies]
lemmy_api = { workspace = true }

View file

@ -22,16 +22,16 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^22.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"eslint": "^9.9.0",
"@types/node": "^22.9.0",
"@typescript-eslint/eslint-plugin": "^8.13.0",
"@typescript-eslint/parser": "^8.13.0",
"eslint": "^9.14.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.20.0-private-community.9",
"lemmy-js-client": "0.20.0-instance-blocks.5",
"prettier": "^3.2.5",
"ts-jest": "^29.1.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.1.0"
"typescript-eslint": "^8.13.0"
}
}

View file

@ -12,38 +12,38 @@ importers:
specifier: ^29.5.12
version: 29.5.14
'@types/node':
specifier: ^22.3.0
version: 22.8.6
specifier: ^22.9.0
version: 22.9.0
'@typescript-eslint/eslint-plugin':
specifier: ^8.1.0
version: 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
specifier: ^8.13.0
version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/parser':
specifier: ^8.1.0
version: 8.12.2(eslint@9.13.0)(typescript@5.6.3)
specifier: ^8.13.0
version: 8.13.0(eslint@9.14.0)(typescript@5.6.3)
eslint:
specifier: ^9.9.0
version: 9.13.0
specifier: ^9.14.0
version: 9.14.0
eslint-plugin-prettier:
specifier: ^5.1.3
version: 5.2.1(eslint@9.13.0)(prettier@3.3.3)
version: 5.2.1(eslint@9.14.0)(prettier@3.3.3)
jest:
specifier: ^29.5.0
version: 29.7.0(@types/node@22.8.6)
version: 29.7.0(@types/node@22.9.0)
lemmy-js-client:
specifier: 0.20.0-private-community.9
version: 0.20.0-private-community.9
specifier: 0.20.0-instance-blocks.5
version: 0.20.0-instance-blocks.5
prettier:
specifier: ^3.2.5
version: 3.3.3
ts-jest:
specifier: ^29.1.0
version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.8.6))(typescript@5.6.3)
version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3)
typescript:
specifier: ^5.5.4
version: 5.6.3
typescript-eslint:
specifier: ^8.1.0
version: 8.12.2(eslint@9.13.0)(typescript@5.6.3)
specifier: ^8.13.0
version: 8.13.0(eslint@9.14.0)(typescript@5.6.3)
packages:
@ -240,8 +240,8 @@ packages:
resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.13.0':
resolution: {integrity: sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==}
'@eslint/js@9.14.0':
resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.4':
@ -268,6 +268,10 @@ packages:
resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
engines: {node: '>=18.18'}
'@humanwhocodes/retry@0.4.1':
resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==}
engines: {node: '>=18.18'}
'@istanbuljs/load-nyc-config@1.1.0':
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'}
@ -418,8 +422,8 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@22.8.6':
resolution: {integrity: sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==}
'@types/node@22.9.0':
resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==}
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
@ -430,8 +434,8 @@ packages:
'@types/yargs@17.0.32':
resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==}
'@typescript-eslint/eslint-plugin@8.12.2':
resolution: {integrity: sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==}
'@typescript-eslint/eslint-plugin@8.13.0':
resolution: {integrity: sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
@ -441,8 +445,8 @@ packages:
typescript:
optional: true
'@typescript-eslint/parser@8.12.2':
resolution: {integrity: sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==}
'@typescript-eslint/parser@8.13.0':
resolution: {integrity: sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@ -451,12 +455,12 @@ packages:
typescript:
optional: true
'@typescript-eslint/scope-manager@8.12.2':
resolution: {integrity: sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==}
'@typescript-eslint/scope-manager@8.13.0':
resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/type-utils@8.12.2':
resolution: {integrity: sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==}
'@typescript-eslint/type-utils@8.13.0':
resolution: {integrity: sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
@ -464,12 +468,12 @@ packages:
typescript:
optional: true
'@typescript-eslint/types@8.12.2':
resolution: {integrity: sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==}
'@typescript-eslint/types@8.13.0':
resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.12.2':
resolution: {integrity: sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==}
'@typescript-eslint/typescript-estree@8.13.0':
resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
@ -477,14 +481,14 @@ packages:
typescript:
optional: true
'@typescript-eslint/utils@8.12.2':
resolution: {integrity: sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==}
'@typescript-eslint/utils@8.13.0':
resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
'@typescript-eslint/visitor-keys@8.12.2':
resolution: {integrity: sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==}
'@typescript-eslint/visitor-keys@8.13.0':
resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
acorn-jsx@5.3.2:
@ -649,6 +653,10 @@ packages:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
cross-spawn@7.0.5:
resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==}
engines: {node: '>= 8'}
debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
@ -737,8 +745,8 @@ packages:
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.13.0:
resolution: {integrity: sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==}
eslint@9.14.0:
resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
@ -1159,8 +1167,8 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
lemmy-js-client@0.20.0-private-community.9:
resolution: {integrity: sha512-iuFezswCzIco5U5Q4Eo8HAWVE65pDW2zeO+fYLEyFl30SLw9a3gqJkip2vbDfVvoAjDXyUskZKddf1Nnj8mVcg==}
lemmy-js-client@0.20.0-instance-blocks.5:
resolution: {integrity: sha512-wDuRFzg32lbbJr4cNmd+cbzjgw+okw2/d5AujYjAm4gv0OEFfsYhP3QQ2WscwUR5HJTdzsR7IIyiBnvmaEUzUw==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -1533,8 +1541,8 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
typescript-eslint@8.12.2:
resolution: {integrity: sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==}
typescript-eslint@8.13.0:
resolution: {integrity: sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
@ -1808,9 +1816,9 @@ snapshots:
'@bcoe/v8-coverage@0.2.3': {}
'@eslint-community/eslint-utils@4.4.1(eslint@9.13.0)':
'@eslint-community/eslint-utils@4.4.1(eslint@9.14.0)':
dependencies:
eslint: 9.13.0
eslint: 9.14.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
@ -1839,7 +1847,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@eslint/js@9.13.0': {}
'@eslint/js@9.14.0': {}
'@eslint/object-schema@2.1.4': {}
@ -1858,6 +1866,8 @@ snapshots:
'@humanwhocodes/retry@0.3.1': {}
'@humanwhocodes/retry@0.4.1': {}
'@istanbuljs/load-nyc-config@1.1.0':
dependencies:
camelcase: 5.3.1
@ -1871,7 +1881,7 @@ snapshots:
'@jest/console@29.7.0':
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
jest-message-util: 29.7.0
jest-util: 29.7.0
@ -1884,14 +1894,14 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.9.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@22.8.6)
jest-config: 29.7.0(@types/node@22.9.0)
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@ -1916,7 +1926,7 @@ snapshots:
dependencies:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-mock: 29.7.0
'@jest/expect-utils@29.7.0':
@ -1934,7 +1944,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@sinonjs/fake-timers': 10.3.0
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-message-util: 29.7.0
jest-mock: 29.7.0
jest-util: 29.7.0
@ -1956,7 +1966,7 @@ snapshots:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.22
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
collect-v8-coverage: 1.0.2
exit: 0.1.2
@ -2026,7 +2036,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 22.8.6
'@types/node': 22.9.0
'@types/yargs': 17.0.32
chalk: 4.1.2
@ -2096,7 +2106,7 @@ snapshots:
'@types/graceful-fs@4.1.9':
dependencies:
'@types/node': 22.8.6
'@types/node': 22.9.0
'@types/istanbul-lib-coverage@2.0.6': {}
@ -2115,7 +2125,7 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/node@22.8.6':
'@types/node@22.9.0':
dependencies:
undici-types: 6.19.8
@ -2127,15 +2137,15 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/scope-manager': 8.12.2
'@typescript-eslint/type-utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.12.2
eslint: 9.13.0
'@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/scope-manager': 8.13.0
'@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.13.0
eslint: 9.14.0
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
@ -2145,28 +2155,28 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.12.2
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.12.2
'@typescript-eslint/scope-manager': 8.13.0
'@typescript-eslint/types': 8.13.0
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.13.0
debug: 4.3.7
eslint: 9.13.0
eslint: 9.14.0
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.12.2':
'@typescript-eslint/scope-manager@8.13.0':
dependencies:
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/visitor-keys': 8.12.2
'@typescript-eslint/types': 8.13.0
'@typescript-eslint/visitor-keys': 8.13.0
'@typescript-eslint/type-utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/type-utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3)
'@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
'@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
debug: 4.3.7
ts-api-utils: 1.4.0(typescript@5.6.3)
optionalDependencies:
@ -2175,12 +2185,12 @@ snapshots:
- eslint
- supports-color
'@typescript-eslint/types@8.12.2': {}
'@typescript-eslint/types@8.13.0': {}
'@typescript-eslint/typescript-estree@8.12.2(typescript@5.6.3)':
'@typescript-eslint/typescript-estree@8.13.0(typescript@5.6.3)':
dependencies:
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/visitor-keys': 8.12.2
'@typescript-eslint/types': 8.13.0
'@typescript-eslint/visitor-keys': 8.13.0
debug: 4.3.7
fast-glob: 3.3.2
is-glob: 4.0.3
@ -2192,20 +2202,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0)
'@typescript-eslint/scope-manager': 8.12.2
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3)
eslint: 9.13.0
'@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0)
'@typescript-eslint/scope-manager': 8.13.0
'@typescript-eslint/types': 8.13.0
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
eslint: 9.14.0
transitivePeerDependencies:
- supports-color
- typescript
'@typescript-eslint/visitor-keys@8.12.2':
'@typescript-eslint/visitor-keys@8.13.0':
dependencies:
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/types': 8.13.0
eslint-visitor-keys: 3.4.3
acorn-jsx@5.3.2(acorn@8.14.0):
@ -2373,13 +2383,13 @@ snapshots:
convert-source-map@2.0.0: {}
create-jest@29.7.0(@types/node@22.8.6):
create-jest@29.7.0(@types/node@22.9.0):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@22.8.6)
jest-config: 29.7.0(@types/node@22.9.0)
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@ -2394,6 +2404,12 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
cross-spawn@7.0.5:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
debug@4.3.7:
dependencies:
ms: 2.1.3
@ -2428,9 +2444,9 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-plugin-prettier@5.2.1(eslint@9.13.0)(prettier@3.3.3):
eslint-plugin-prettier@5.2.1(eslint@9.14.0)(prettier@3.3.3):
dependencies:
eslint: 9.13.0
eslint: 9.14.0
prettier: 3.3.3
prettier-linter-helpers: 1.0.0
synckit: 0.9.1
@ -2444,23 +2460,23 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
eslint@9.13.0:
eslint@9.14.0:
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0)
'@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0)
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.18.0
'@eslint/core': 0.7.0
'@eslint/eslintrc': 3.1.0
'@eslint/js': 9.13.0
'@eslint/js': 9.14.0
'@eslint/plugin-kit': 0.2.2
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.3.1
'@humanwhocodes/retry': 0.4.1
'@types/estree': 1.0.6
'@types/json-schema': 7.0.15
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
cross-spawn: 7.0.5
debug: 4.3.7
escape-string-regexp: 4.0.0
eslint-scope: 8.2.0
@ -2736,7 +2752,7 @@ snapshots:
'@jest/expect': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
co: 4.6.0
dedent: 1.5.1
@ -2756,16 +2772,16 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-cli@29.7.0(@types/node@22.8.6):
jest-cli@29.7.0(@types/node@22.9.0):
dependencies:
'@jest/core': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@22.8.6)
create-jest: 29.7.0(@types/node@22.9.0)
exit: 0.1.2
import-local: 3.1.0
jest-config: 29.7.0(@types/node@22.8.6)
jest-config: 29.7.0(@types/node@22.9.0)
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@ -2775,7 +2791,7 @@ snapshots:
- supports-color
- ts-node
jest-config@29.7.0(@types/node@22.8.6):
jest-config@29.7.0(@types/node@22.9.0):
dependencies:
'@babel/core': 7.23.9
'@jest/test-sequencer': 29.7.0
@ -2800,7 +2816,7 @@ snapshots:
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.8.6
'@types/node': 22.9.0
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@ -2829,7 +2845,7 @@ snapshots:
'@jest/environment': 29.7.0
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-mock: 29.7.0
jest-util: 29.7.0
@ -2839,7 +2855,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@types/graceful-fs': 4.1.9
'@types/node': 22.8.6
'@types/node': 22.9.0
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
@ -2878,7 +2894,7 @@ snapshots:
jest-mock@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-util: 29.7.0
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@ -2913,7 +2929,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.11
@ -2941,7 +2957,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
cjs-module-lexer: 1.2.3
collect-v8-coverage: 1.0.2
@ -2987,7 +3003,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@ -3006,7 +3022,7 @@ snapshots:
dependencies:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@ -3015,17 +3031,17 @@ snapshots:
jest-worker@29.7.0:
dependencies:
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
jest@29.7.0(@types/node@22.8.6):
jest@29.7.0(@types/node@22.9.0):
dependencies:
'@jest/core': 29.7.0
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0(@types/node@22.8.6)
jest-cli: 29.7.0(@types/node@22.9.0)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -3061,7 +3077,7 @@ snapshots:
kleur@3.0.3: {}
lemmy-js-client@0.20.0-private-community.9: {}
lemmy-js-client@0.20.0-instance-blocks.5: {}
leven@3.1.0: {}
@ -3342,12 +3358,12 @@ snapshots:
dependencies:
typescript: 5.6.3
ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.8.6))(typescript@5.6.3):
ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@22.8.6)
jest: 29.7.0(@types/node@22.9.0)
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
@ -3371,11 +3387,11 @@ snapshots:
type-fest@0.21.3: {}
typescript-eslint@8.12.2(eslint@9.13.0)(typescript@5.6.3):
typescript-eslint@8.13.0(eslint@9.14.0)(typescript@5.6.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/parser': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies:

View file

@ -25,16 +25,16 @@ import {
getComments,
createComment,
getCommunityByName,
blockInstance,
waitUntil,
alphaUrl,
delta,
betaAllowedInstances,
searchPostLocal,
longDelay,
editCommunity,
unfollows,
userBlockInstance,
} from "./shared";
import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams";
import { EditCommunity, EditSite } from "lemmy-js-client";
beforeAll(setupLogins);
@ -363,7 +363,7 @@ test("User blocks instance, communities are hidden", async () => {
expect(listing_ids).toContain(postRes.post_view.post.ap_id);
// block the beta instance
await blockInstance(alpha, alphaPost.community.instance_id, true);
await userBlockInstance(alpha, alphaPost.community.instance_id, true);
// after blocking, post should not be in listing
let listing2 = await getPosts(alpha, "All");
@ -371,7 +371,7 @@ test("User blocks instance, communities are hidden", async () => {
expect(listing_ids2.indexOf(postRes.post_view.post.ap_id)).toBe(-1);
// unblock instance again
await blockInstance(alpha, alphaPost.community.instance_id, false);
await userBlockInstance(alpha, alphaPost.community.instance_id, false);
// post should be included in listing
let listing3 = await getPosts(alpha, "All");
@ -455,9 +455,12 @@ test("Dont receive community activities after unsubscribe", async () => {
expect(communityRes1.community_view.counts.subscribers).toBe(2);
// temporarily block alpha, so that it doesn't know about unfollow
let editSiteForm: EditSite = {};
editSiteForm.allowed_instances = ["lemmy-epsilon"];
await beta.editSite(editSiteForm);
var allow_instance_params: AdminAllowInstanceParams = {
instance: "lemmy-alpha",
allow: false,
reason: undefined,
};
await beta.adminAllowInstance(allow_instance_params);
await longDelay();
// unfollow
@ -471,8 +474,8 @@ test("Dont receive community activities after unsubscribe", async () => {
expect(communityRes2.community_view.counts.subscribers).toBe(2);
// unblock alpha
editSiteForm.allowed_instances = betaAllowedInstances;
await beta.editSite(editSiteForm);
allow_instance_params.allow = true;
await beta.adminAllowInstance(allow_instance_params);
await longDelay();
// create a post, it shouldnt reach beta

View file

@ -41,6 +41,9 @@ afterAll(async () => {
});
test("Upload image and delete it", async () => {
const healthz = await fetch(alphaUrl + "/pictrs/healthz");
expect(healthz.status).toBe(200);
// Before running this test, you need to delete all previous images in the DB
await deleteAllImages(alpha);

View file

@ -40,6 +40,7 @@ import {
createCommunity,
} from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView";
import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams";
import { EditSite, ResolveObject } from "lemmy-js-client";
let betaCommunity: CommunityView | undefined;
@ -87,12 +88,12 @@ async function assertPostFederation(
}
test("Create a post", async () => {
// Setup some allowlists and blocklists
const editSiteForm: EditSite = {};
editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = ["lemmy-alpha"];
await epsilon.editSite(editSiteForm);
// Block alpha
var block_instance_params: AdminBlockInstanceParams = {
instance: "lemmy-alpha",
block: true,
};
await epsilon.adminBlockInstance(block_instance_params);
if (!betaCommunity) {
throw "Missing beta community";
@ -132,11 +133,9 @@ test("Create a post", async () => {
resolvePost(epsilon, postRes.post_view.post),
).rejects.toStrictEqual(Error("not_found"));
// remove added allow/blocklists
editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = [];
await delta.editSite(editSiteForm);
await epsilon.editSite(editSiteForm);
// remove blocked instance
block_instance_params.block = false;
await epsilon.adminBlockInstance(block_instance_params);
});
test("Create a post in a non-existent community", async () => {

View file

@ -1,6 +1,6 @@
jest.setTimeout(120000);
import { FollowCommunity } from "lemmy-js-client";
import { FollowCommunity, LemmyHttp } from "lemmy-js-client";
import {
alpha,
setupLogins,
@ -21,6 +21,9 @@ import {
resolveComment,
likeComment,
waitUntil,
gamma,
getPosts,
getComments,
} from "./shared";
beforeAll(setupLogins);
@ -47,6 +50,7 @@ test("Follow a private community", async () => {
await resolveCommunity(user, community.community_view.community.actor_id)
).community;
expect(betaCommunity).toBeDefined();
expect(betaCommunity?.community.visibility).toBe("Private");
const betaCommunityId = betaCommunity!.community.id;
const follow_form: FollowCommunity = {
community_id: betaCommunityId,
@ -148,16 +152,7 @@ test("Only followers can view and interact with private community content", asyn
follow: true,
};
await user.followCommunity(follow_form);
const pendingFollows1 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
const approve = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows1.items[0].person.id,
);
expect(approve.success).toBe(true);
approveFollower(alpha, alphaCommunityId);
// now user can fetch posts and comments in community (using signed fetch), and create posts
await waitUntil(
@ -212,3 +207,151 @@ test("Reject follower", async () => {
c => c.community_view.subscribed == "NotSubscribed",
);
});
test("Follow a private community and receive activities", async () => {
// create private community
const community = await createCommunity(alpha, randomString(10), "Private");
expect(community.community_view.community.visibility).toBe("Private");
const alphaCommunityId = community.community_view.community.id;
// follow with users from beta and gamma
const betaCommunity = (
await resolveCommunity(beta, community.community_view.community.actor_id)
).community;
expect(betaCommunity).toBeDefined();
const betaCommunityId = betaCommunity!.community.id;
const follow_form_beta: FollowCommunity = {
community_id: betaCommunityId,
follow: true,
};
await beta.followCommunity(follow_form_beta);
await approveFollower(alpha, alphaCommunityId);
const gammaCommunityId = (
await resolveCommunity(gamma, community.community_view.community.actor_id)
).community!.community.id;
const follow_form_gamma: FollowCommunity = {
community_id: gammaCommunityId,
follow: true,
};
await gamma.followCommunity(follow_form_gamma);
await approveFollower(alpha, alphaCommunityId);
// Follow is confirmed
await waitUntil(
() => getCommunity(beta, betaCommunityId),
c => c.community_view.subscribed == "Subscribed",
);
await waitUntil(
() => getCommunity(gamma, gammaCommunityId),
c => c.community_view.subscribed == "Subscribed",
);
// create a post and comment from gamma
const post = await createPost(gamma, gammaCommunityId);
const post_id = post.post_view.post.id;
expect(post_id).toBeDefined();
const comment = await createComment(gamma, post_id);
const comment_id = comment.comment_view.comment.id;
expect(comment_id).toBeDefined();
// post and comment were federated to beta
let posts = await waitUntil(
() => getPosts(beta, "All", betaCommunityId),
c => c.posts.length == 1,
);
expect(posts.posts[0].post.ap_id).toBe(post.post_view.post.ap_id);
expect(posts.posts[0].post.name).toBe(post.post_view.post.name);
let comments = await waitUntil(
() => getComments(beta, posts.posts[0].post.id),
c => c.comments.length == 1,
);
expect(comments.comments[0].comment.ap_id).toBe(
comment.comment_view.comment.ap_id,
);
expect(comments.comments[0].comment.content).toBe(
comment.comment_view.comment.content,
);
});
test("Fetch remote content in private community", async () => {
// create private community
const community = await createCommunity(alpha, randomString(10), "Private");
expect(community.community_view.community.visibility).toBe("Private");
const alphaCommunityId = community.community_view.community.id;
const betaCommunityId = (
await resolveCommunity(beta, community.community_view.community.actor_id)
).community!.community.id;
const follow_form_beta: FollowCommunity = {
community_id: betaCommunityId,
follow: true,
};
await beta.followCommunity(follow_form_beta);
await approveFollower(alpha, alphaCommunityId);
// Follow is confirmed
await waitUntil(
() => getCommunity(beta, betaCommunityId),
c => c.community_view.subscribed == "Subscribed",
);
// beta creates post and comment
const post = await createPost(beta, betaCommunityId);
const post_id = post.post_view.post.id;
expect(post_id).toBeDefined();
const comment = await createComment(beta, post_id);
const comment_id = comment.comment_view.comment.id;
expect(comment_id).toBeDefined();
// Wait for it to federate
await waitUntil(
() => resolveComment(alpha, comment.comment_view.comment),
p => p?.comment?.comment.id != undefined,
);
// create gamma user
const gammaCommunityId = (
await resolveCommunity(gamma, community.community_view.community.actor_id)
).community!.community.id;
const follow_form: FollowCommunity = {
community_id: gammaCommunityId,
follow: true,
};
// cannot fetch post yet
await expect(resolvePost(gamma, post.post_view.post)).rejects.toStrictEqual(
Error("not_found"),
);
// follow community and approve
await gamma.followCommunity(follow_form);
await approveFollower(alpha, alphaCommunityId);
// now user can fetch posts and comments in community (using signed fetch), and create posts.
// for this to work, beta checks with alpha if gamma is really an approved follower.
let resolvedPost = await waitUntil(
() => resolvePost(gamma, post.post_view.post),
p => p?.post?.post.id != undefined,
);
expect(resolvedPost.post?.post.ap_id).toBe(post.post_view.post.ap_id);
const resolvedComment = await waitUntil(
() => resolveComment(gamma, comment.comment_view.comment),
p => p?.comment?.comment.id != undefined,
);
expect(resolvedComment?.comment?.comment.ap_id).toBe(
comment.comment_view.comment.ap_id,
);
});
async function approveFollower(user: LemmyHttp, community_id: number) {
let pendingFollows1 = await waitUntil(
() => listCommunityPendingFollows(user),
f => f.items.length == 1,
);
const approve = await approveCommunityPendingFollow(
alpha,
community_id,
pendingFollows1.items[0].person.id,
);
expect(approve.success).toBe(true);
}

View file

@ -1,15 +1,13 @@
import {
AdminBlockInstanceParams,
ApproveCommunityPendingFollower,
BlockCommunity,
BlockCommunityResponse,
BlockInstance,
BlockInstanceResponse,
CommunityId,
CommunityVisibility,
CreatePrivateMessageReport,
DeleteImage,
EditCommunity,
GetCommunityPendingFollowsCount,
GetCommunityPendingFollowsCountResponse,
GetReplies,
GetRepliesResponse,
@ -22,11 +20,13 @@ import {
PostView,
PrivateMessageReportResponse,
SuccessResponse,
UserBlockInstanceParams,
} from "lemmy-js-client";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
import { DeletePost } from "lemmy-js-client/dist/types/DeletePost";
import { EditPost } from "lemmy-js-client/dist/types/EditPost";
import { EditSite } from "lemmy-js-client/dist/types/EditSite";
import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams";
import { FeaturePost } from "lemmy-js-client/dist/types/FeaturePost";
import { GetComments } from "lemmy-js-client/dist/types/GetComments";
import { GetCommentsResponse } from "lemmy-js-client/dist/types/GetCommentsResponse";
@ -105,13 +105,6 @@ export const gamma = new LemmyHttp(gammaUrl, { fetchFunction });
export const delta = new LemmyHttp(deltaUrl, { fetchFunction });
export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
export const betaAllowedInstances = [
"lemmy-alpha",
"lemmy-gamma",
"lemmy-delta",
"lemmy-epsilon",
];
const password = "lemmylemmy";
export async function setupLogins() {
@ -169,30 +162,29 @@ export async function setupLogins() {
rate_limit_comment: 999,
rate_limit_search: 999,
};
// Set the blocks and auths for each
editSiteForm.allowed_instances = [
"lemmy-beta",
"lemmy-gamma",
"lemmy-delta",
"lemmy-epsilon",
];
await alpha.editSite(editSiteForm);
editSiteForm.allowed_instances = betaAllowedInstances;
await beta.editSite(editSiteForm);
editSiteForm.allowed_instances = [
"lemmy-alpha",
"lemmy-beta",
"lemmy-delta",
"lemmy-epsilon",
];
await gamma.editSite(editSiteForm);
// Setup delta allowed instance
editSiteForm.allowed_instances = ["lemmy-beta"];
await delta.editSite(editSiteForm);
await epsilon.editSite(editSiteForm);
// Set the blocks for each
await allowInstance(alpha, "lemmy-beta");
await allowInstance(alpha, "lemmy-gamma");
await allowInstance(alpha, "lemmy-delta");
await allowInstance(alpha, "lemmy-epsilon");
await allowInstance(beta, "lemmy-alpha");
await allowInstance(beta, "lemmy-gamma");
await allowInstance(beta, "lemmy-delta");
await allowInstance(beta, "lemmy-epsilon");
await allowInstance(gamma, "lemmy-alpha");
await allowInstance(gamma, "lemmy-beta");
await allowInstance(gamma, "lemmy-delta");
await allowInstance(gamma, "lemmy-epsilon");
await allowInstance(delta, "lemmy-beta");
// Create the main alpha/beta communities
// Ignore thrown errors of duplicates
@ -209,6 +201,17 @@ export async function setupLogins() {
}
}
async function allowInstance(api: LemmyHttp, instance: string) {
const params: AdminAllowInstanceParams = {
instance,
allow: true,
};
// Ignore errors from duplicate allows (because setup gets called for each test file)
try {
await api.adminAllowInstance(params);
} catch {}
}
export async function createPost(
api: LemmyHttp,
community_id: number,
@ -855,16 +858,16 @@ export function getPosts(
return api.getPosts(form);
}
export function blockInstance(
export function userBlockInstance(
api: LemmyHttp,
instance_id: InstanceId,
block: boolean,
): Promise<BlockInstanceResponse> {
let form: BlockInstance = {
): Promise<SuccessResponse> {
let form: UserBlockInstanceParams = {
instance_id,
block,
};
return api.blockInstance(form);
return api.userBlockInstance(form);
}
export function blockCommunity(
@ -988,7 +991,7 @@ export function getCommentParentId(comment: Comment): number | undefined {
if (split.length > 1) {
return Number(split[split.length - 2]);
} else {
console.log(`Failed to extract comment parent id from ${comment.path}`);
console.error(`Failed to extract comment parent id from ${comment.path}`);
return undefined;
}
}
@ -1006,7 +1009,7 @@ export async function waitUntil<T>(
result = await fetcher();
if (checker(result)) return result;
} catch (error) {
//console.error(error);
console.error(error);
}
await delay(
delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000,

View file

@ -23,7 +23,12 @@ import {
unfollows,
saveUserSettingsBio,
} from "./shared";
import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client";
import {
EditSite,
LemmyHttp,
SaveUserSettings,
UploadImage,
} from "lemmy-js-client";
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
beforeAll(setupLogins);
@ -149,9 +154,14 @@ test("Create user with Arabic name", async () => {
});
test("Create user with accept-language", async () => {
const edit: EditSite = {
discussion_languages: [32],
};
await alpha.editSite(edit);
let lemmy_http = new LemmyHttp(alphaUrl, {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax
headers: { "Accept-Language": "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5" },
headers: { "Accept-Language": "fr-CH, en;q=0.8, *;q=0.5" },
});
let user = await registerUser(lemmy_http, alphaUrl);

View file

@ -73,6 +73,15 @@
#
# Requires pict-rs 0.5
"ProxyAllImages"
# Allows bypassing proxy for specific image hosts when using ProxyAllImages.
#
# imgur.com is bypassed by default to avoid rate limit errors. When specifying any bypass
# in the config, this default is ignored and you need to list imgur explicitly. To proxy imgur
# requests, specify a noop bypass list, eg `proxy_bypass_domains ["example.org"]`.
proxy_bypass_domains: [
"i.imgur.com"
/* ... */
]
# Timeout for uploading images to pictrs (in seconds)
upload_timeout: 30
# Resize post thumbnails to this maximum width/height.
@ -122,5 +131,5 @@
}
# Sets a response Access-Control-Allow-Origin CORS header
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
cors_origin: "*"
cors_origin: "lemmy.tld"
}

View file

@ -34,7 +34,7 @@ tracing = { workspace = true }
chrono = { workspace = true }
url = { workspace = true }
hound = "3.5.1"
sitemap-rs = "0.2.1"
sitemap-rs = "0.2.2"
totp-rs = { version = "5.6.0", features = ["gen_secret", "otpauth"] }
actix-web-httpauth = "0.8.2"

View file

@ -10,7 +10,7 @@ use lemmy_db_schema::{
source::{
community::{Community, CommunityModerator, CommunityModeratorForm},
local_user::LocalUser,
moderator::{ModAddCommunity, ModAddCommunityForm},
mod_log::moderator::{ModAddCommunity, ModAddCommunityForm},
},
traits::{Crud, Joinable},
};

View file

@ -20,7 +20,7 @@ use lemmy_db_schema::{
CommunityPersonBanForm,
},
local_user::LocalUser,
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
},
traits::{Bannable, Crud, Followable},
};

View file

@ -10,7 +10,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
community::{Community, CommunityUpdateForm},
moderator::{ModHideCommunity, ModHideCommunityForm},
mod_log::moderator::{ModHideCommunity, ModHideCommunityForm},
},
traits::Crud,
};

View file

@ -8,7 +8,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModTransferCommunity, ModTransferCommunityForm},
mod_log::moderator::{ModTransferCommunity, ModTransferCommunityForm},
},
traits::{Crud, Joinable},
};

View file

@ -19,7 +19,7 @@ use lemmy_db_schema::{
CommunityPersonBanForm,
},
local_site::LocalSite,
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
person::Person,
},
traits::{Bannable, Crud, Followable},
@ -145,7 +145,7 @@ fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> LemmyResult<T
let sec = Secret::Raw(secret.as_bytes().to_vec());
let sec_bytes = sec
.to_bytes()
.map_err(|_| LemmyErrorType::CouldntParseTotpSecret)?;
.with_lemmy_type(LemmyErrorType::CouldntParseTotpSecret)?;
TOTP::new(
totp_rs::Algorithm::SHA1,

View file

@ -7,7 +7,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm},
mod_log::moderator::{ModAdd, ModAddForm},
},
traits::Crud,
};
@ -37,7 +37,7 @@ pub async fn add_admin(
// Make sure that the person_id added is local
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
.await
.map_err(|_| LemmyErrorType::ObjectNotLocal)?;
.with_lemmy_type(LemmyErrorType::ObjectNotLocal)?;
LocalUser::update(
&mut context.pool(),

View file

@ -11,7 +11,7 @@ use lemmy_db_schema::{
source::{
local_user::LocalUser,
login_token::LoginToken,
moderator::{ModBan, ModBanForm},
mod_log::moderator::{ModBan, ModBanForm},
person::{Person, PersonUpdateForm},
},
traits::Crud,

View file

@ -10,7 +10,7 @@ use lemmy_db_schema::source::{
login_token::LoginToken,
password_reset_request::PasswordResetRequest,
};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn change_password_after_reset(
@ -32,9 +32,7 @@ pub async fn change_password_after_reset(
// Update the user with the new password
let password = data.password.clone();
LocalUser::update_password(&mut context.pool(), local_user_id, &password)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
LocalUser::update_password(&mut context.pool(), local_user_id, &password).await?;
LoginToken::invalidate_all(&mut context.pool(), local_user_id).await?;

View file

@ -12,6 +12,7 @@ use captcha::{gen, Difficulty};
use lemmy_api_common::{
context::LemmyContext,
person::{CaptchaResponse, GetCaptchaResponse},
LemmyErrorType,
};
use lemmy_db_schema::source::{
captcha_answer::{CaptchaAnswer, CaptchaAnswerForm},
@ -37,7 +38,9 @@ pub async fn get_captcha(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
let answer = captcha.chars_as_string();
let png = captcha.as_base64().expect("failed to generate captcha");
let png = captcha
.as_base64()
.ok_or(LemmyErrorType::CouldntCreateImageCaptcha)?;
let wav = captcha_as_wav_base64(&captcha)?;

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
SuccessResponse,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn reset_password(
@ -17,7 +17,7 @@ pub async fn reset_password(
let email = data.email.to_lowercase();
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
.await
.map_err(|_| LemmyErrorType::IncorrectLogin)?;
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
check_email_verified(&local_user_view, &site_view)?;

View file

@ -143,6 +143,7 @@ pub async fn save_user_settings(
enable_animated_images: data.enable_animated_images,
enable_private_messages: data.enable_private_messages,
collapse_bot_comments: data.collapse_bot_comments,
auto_mark_fetched_posts_as_read: data.auto_mark_fetched_posts_as_read,
..Default::default()
};

View file

@ -10,7 +10,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
community::Community,
moderator::{ModFeaturePost, ModFeaturePostForm},
mod_log::moderator::{ModFeaturePost, ModFeaturePostForm},
post::{Post, PostUpdateForm},
},
traits::Crud,

View file

@ -1,34 +1,39 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::HidePost, SuccessResponse};
use lemmy_api_common::{
context::LemmyContext,
post::{HidePost, PostResponse},
};
use lemmy_db_schema::source::post::PostHide;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
use std::collections::HashSet;
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn hide_post(
data: Json<HidePost>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let post_ids = HashSet::from_iter(data.post_ids.clone());
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
) -> LemmyResult<Json<PostResponse>> {
let person_id = local_user_view.person.id;
let post_id = data.post_id;
// Mark the post as hidden / unhidden
if data.hide {
PostHide::hide(&mut context.pool(), post_ids, person_id)
PostHide::hide(&mut context.pool(), post_id, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
} else {
PostHide::unhide(&mut context.pool(), post_ids, person_id)
PostHide::unhide(&mut context.pool(), post_id, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
}
Ok(Json(SuccessResponse::default()))
let post_view = PostView::read(
&mut context.pool(),
post_id,
Some(&local_user_view.local_user),
false,
)
.await?;
Ok(Json(PostResponse { post_view }))
}

View file

@ -5,18 +5,12 @@ use lemmy_api_common::{
context::LemmyContext,
post::{CreatePostLike, PostResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_bot_account,
check_community_user_action,
check_local_vote_mode,
mark_post_as_read,
VoteItem,
},
utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
};
use lemmy_db_schema::{
source::{
local_site::LocalSite,
post::{PostLike, PostLikeForm},
post::{PostLike, PostLikeForm, PostRead, PostReadForm},
},
traits::Likeable,
};
@ -53,11 +47,7 @@ pub async fn like_post(
)
.await?;
let like_form = PostLikeForm {
post_id: data.post_id,
person_id: local_user_view.person.id,
score: data.score,
};
let like_form = PostLikeForm::new(data.post_id, local_user_view.person.id, data.score);
// Remove any likes first
let person_id = local_user_view.person.id;
@ -72,7 +62,9 @@ pub async fn like_post(
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
}
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
// Mark Post Read
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment {

View file

@ -9,7 +9,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
source::{
moderator::{ModLockPost, ModLockPostForm},
mod_log::moderator::{ModLockPost, ModLockPostForm},
post::{Post, PostUpdateForm},
},
traits::Crud,

View file

@ -0,0 +1,24 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::MarkManyPostsAsRead, SuccessResponse};
use lemmy_db_schema::source::post::PostRead;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
#[tracing::instrument(skip(context))]
pub async fn mark_posts_as_read(
data: Json<MarkManyPostsAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let post_ids = &data.post_ids;
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
let person_id = local_user_view.person.id;
// Mark the posts as read
PostRead::mark_many_as_read(&mut context.pool(), post_ids, person_id).await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,34 +1,35 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse};
use lemmy_db_schema::source::post::PostRead;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
use std::collections::HashSet;
use lemmy_api_common::{
context::LemmyContext,
post::{MarkPostAsRead, PostResponse},
};
use lemmy_db_schema::source::post::{PostRead, PostReadForm};
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn mark_post_as_read(
data: Json<MarkPostAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let post_ids = HashSet::from_iter(data.post_ids.clone());
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
) -> LemmyResult<Json<PostResponse>> {
let person_id = local_user_view.person.id;
let post_id = data.post_id;
// Mark the post as read / unread
let form = PostReadForm::new(post_id, person_id);
if data.read {
PostRead::mark_as_read(&mut context.pool(), post_ids, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
PostRead::mark_as_read(&mut context.pool(), &form).await?;
} else {
PostRead::mark_as_unread(&mut context.pool(), post_ids, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
PostRead::mark_as_unread(&mut context.pool(), &form).await?;
}
let post_view = PostView::read(
&mut context.pool(),
post_id,
Some(&local_user_view.local_user),
false,
)
.await?;
Ok(Json(SuccessResponse::default()))
Ok(Json(PostResponse { post_view }))
}

View file

@ -4,5 +4,6 @@ pub mod hide;
pub mod like;
pub mod list_post_likes;
pub mod lock;
pub mod mark_many_read;
pub mod mark_read;
pub mod save;

View file

@ -2,10 +2,9 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
post::{PostResponse, SavePost},
utils::mark_post_as_read,
};
use lemmy_db_schema::{
source::post::{PostSaved, PostSavedForm},
source::post::{PostRead, PostReadForm, PostSaved, PostSavedForm},
traits::Saveable,
};
use lemmy_db_views::structs::{LocalUserView, PostView};
@ -17,10 +16,7 @@ pub async fn save_post(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let post_saved_form = PostSavedForm {
post_id: data.post_id,
person_id: local_user_view.person.id,
};
let post_saved_form = PostSavedForm::new(data.post_id, local_user_view.person.id);
if data.save {
PostSaved::save(&mut context.pool(), &post_saved_form)
@ -42,7 +38,8 @@ pub async fn save_post(
)
.await?;
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
Ok(Json(PostResponse { post_view }))
}

View file

@ -0,0 +1,53 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
site::AdminAllowInstanceParams,
utils::is_admin,
LemmyErrorType,
SuccessResponse,
};
use lemmy_db_schema::source::{
federation_allowlist::{FederationAllowList, FederationAllowListForm},
instance::Instance,
mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn admin_allow_instance(
data: Json<AdminAllowInstanceParams>,
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
is_admin(&local_user_view)?;
let blocklist = Instance::blocklist(&mut context.pool()).await?;
if !blocklist.is_empty() {
Err(LemmyErrorType::CannotCombineFederationBlocklistAndAllowlist)?;
}
let instance_id = Instance::read_or_create(&mut context.pool(), data.instance.clone())
.await?
.id;
let form = FederationAllowListForm {
instance_id,
updated: None,
};
if data.allow {
FederationAllowList::allow(&mut context.pool(), &form).await?;
} else {
FederationAllowList::unallow(&mut context.pool(), instance_id).await?;
}
let mod_log_form = AdminAllowInstanceForm {
instance_id,
admin_person_id: local_user_view.person.id,
reason: data.reason.clone(),
allowed: data.allow,
};
AdminAllowInstance::insert(&mut context.pool(), &mod_log_form).await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -0,0 +1,56 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
site::AdminBlockInstanceParams,
utils::is_admin,
LemmyErrorType,
SuccessResponse,
};
use lemmy_db_schema::source::{
federation_blocklist::{FederationBlockList, FederationBlockListForm},
instance::Instance,
mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn admin_block_instance(
data: Json<AdminBlockInstanceParams>,
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
is_admin(&local_user_view)?;
let allowlist = Instance::allowlist(&mut context.pool()).await?;
if !allowlist.is_empty() {
Err(LemmyErrorType::CannotCombineFederationBlocklistAndAllowlist)?;
}
let instance_id = Instance::read_or_create(&mut context.pool(), data.instance.clone())
.await?
.id;
let form = FederationBlockListForm {
instance_id,
expires: data.expires,
updated: None,
};
if data.block {
FederationBlockList::block(&mut context.pool(), &form).await?;
} else {
FederationBlockList::unblock(&mut context.pool(), instance_id).await?;
}
let mod_log_form = AdminBlockInstanceForm {
instance_id,
admin_person_id: local_user_view.person.id,
blocked: data.block,
reason: data.reason.clone(),
when_: data.expires,
};
AdminBlockInstance::insert(&mut context.pool(), &mod_log_form).await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -6,7 +6,7 @@ use lemmy_db_schema::{
language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm},
mod_log::moderator::{ModAdd, ModAddForm},
oauth_provider::OAuthProvider,
tagline::Tagline,
},

View file

@ -1,7 +1,9 @@
pub mod block;
pub mod admin_allow_instance;
pub mod admin_block_instance;
pub mod federated_instances;
pub mod leave_admin;
pub mod list_all_media;
pub mod mod_log;
pub mod purge;
pub mod registration_applications;
pub mod user_block_instance;

View file

@ -7,6 +7,8 @@ use lemmy_api_common::{
use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_moderator::structs::{
AdminAllowInstanceView,
AdminBlockInstanceView,
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
@ -121,6 +123,8 @@ pub async fn get_mod_log(
admin_purged_communities,
admin_purged_posts,
admin_purged_comments,
admin_block_instance,
admin_allow_instance,
) = if data.community_id.is_none() {
(
match type_ {
@ -161,6 +165,18 @@ pub async fn get_mod_log(
}
_ => Default::default(),
},
match type_ {
All | AdminBlockInstance if other_person_id.is_none() => {
AdminBlockInstanceView::list(&mut context.pool(), params).await?
}
_ => Default::default(),
},
match type_ {
All | AdminAllowInstance if other_person_id.is_none() => {
AdminAllowInstanceView::list(&mut context.pool(), params).await?
}
_ => Default::default(),
},
)
} else {
Default::default()
@ -183,5 +199,7 @@ pub async fn get_mod_log(
admin_purged_posts,
admin_purged_comments,
hidden_communities,
admin_block_instance,
admin_allow_instance,
}))
}

View file

@ -11,7 +11,7 @@ use lemmy_db_schema::{
source::{
comment::Comment,
local_user::LocalUser,
moderator::{AdminPurgeComment, AdminPurgeCommentForm},
mod_log::admin::{AdminPurgeComment, AdminPurgeCommentForm},
},
traits::Crud,
};

View file

@ -13,7 +13,7 @@ use lemmy_db_schema::{
source::{
community::Community,
local_user::LocalUser,
moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm},
mod_log::admin::{AdminPurgeCommunity, AdminPurgeCommunityForm},
},
traits::Crud,
};

View file

@ -11,7 +11,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
local_user::LocalUser,
moderator::{AdminPurgePerson, AdminPurgePersonForm},
mod_log::admin::{AdminPurgePerson, AdminPurgePersonForm},
person::{Person, PersonUpdateForm},
},
traits::Crud,

View file

@ -11,7 +11,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
local_user::LocalUser,
moderator::{AdminPurgePost, AdminPurgePostForm},
mod_log::admin::{AdminPurgePost, AdminPurgePostForm},
post::Post,
},
traits::Crud,

View file

@ -1,9 +1,6 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
site::{BlockInstance, BlockInstanceResponse},
};
use lemmy_api_common::{context::LemmyContext, site::UserBlockInstanceParams, SuccessResponse};
use lemmy_db_schema::{
source::instance_block::{InstanceBlock, InstanceBlockForm},
traits::Blockable,
@ -12,11 +9,11 @@ use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn block_instance(
data: Json<BlockInstance>,
pub async fn user_block_instance(
data: Json<UserBlockInstanceParams>,
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> LemmyResult<Json<BlockInstanceResponse>> {
) -> LemmyResult<Json<SuccessResponse>> {
let instance_id = data.instance_id;
let person_id = local_user_view.person.id;
if local_user_view.person.instance_id == instance_id {
@ -38,7 +35,5 @@ pub async fn block_instance(
.with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?;
}
Ok(Json(BlockInstanceResponse {
blocked: data.block,
}))
Ok(Json(SuccessResponse::default()))
}

View file

@ -36,6 +36,7 @@ full = [
"futures",
"jsonwebtoken",
"mime",
"moka",
]
[dependencies]
@ -58,22 +59,18 @@ uuid = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true }
ts-rs = { workspace = true, optional = true }
moka.workspace = true
moka = { workspace = true, optional = true }
anyhow.workspace = true
actix-web = { workspace = true, optional = true }
enum-map = { workspace = true }
urlencoding = { workspace = true }
mime = { version = "0.3.17", optional = true }
mime_guess = "2.0.5"
webpage = { version = "2.0", default-features = false, features = [
"serde",
], optional = true }
encoding_rs = { version = "0.8.34", optional = true }
encoding_rs = { version = "0.8.35", optional = true }
jsonwebtoken = { version = "9.3.0", optional = true }
# necessary for wasmt compilation
getrandom = { version = "0.2.15", features = ["js"] }
[package.metadata.cargo-shear]
ignored = ["getrandom"]
[dev-dependencies]
serial_test = { workspace = true }

View file

@ -17,8 +17,10 @@ use lemmy_db_schema::{
actor_language::CommunityLanguage,
comment::Comment,
comment_reply::{CommentReply, CommentReplyInsertForm},
community::Community,
person::Person,
person_mention::{PersonMention, PersonMentionInsertForm},
post::Post,
},
traits::Crud,
};
@ -101,17 +103,28 @@ pub async fn send_local_notifs(
let mut recipient_ids = Vec::new();
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
// let person = my_local_user.person;
// Read the comment view to get extra info
let comment_view = CommentView::read(
&mut context.pool(),
comment_id,
local_user_view.map(|view| &view.local_user),
)
.await?;
let comment = comment_view.comment;
let post = comment_view.post;
let community = comment_view.community;
// When called from api code, we have local user view and can read with CommentView
// to reduce db queries. But when receiving a federated comment the user view is None,
// which means that comments inside private communities cant be read. As a workaround
// we need to read the items manually to bypass this check.
let (comment, post, community) = if let Some(local_user_view) = local_user_view {
let comment_view = CommentView::read(
&mut context.pool(),
comment_id,
Some(&local_user_view.local_user),
)
.await?;
(
comment_view.comment,
comment_view.post,
comment_view.community,
)
} else {
let comment = Comment::read(&mut context.pool(), comment_id).await?;
let post = Post::read(&mut context.pool(), comment.post_id).await?;
let community = Community::read(&mut context.pool(), post.community_id).await?;
(comment, post, community)
};
// Send the local mentions
for mention in mentions

View file

@ -55,6 +55,7 @@ impl LemmyContext {
/// Initialize a context for use in tests which blocks federation network calls.
///
/// Do not use this in production code.
#[allow(clippy::expect_used)]
pub async fn init_test_federation_config() -> FederationConfig<LemmyContext> {
// call this to run migrations
let pool = build_db_pool_for_tests();

View file

@ -178,6 +178,9 @@ pub struct SaveUserSettings {
pub show_downvotes: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub show_upvote_percentage: Option<bool>,
/// Whether to automatically mark fetched posts as read.
#[cfg_attr(feature = "full", ts(optional))]
pub auto_mark_fetched_posts_as_read: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]

View file

@ -109,6 +109,9 @@ pub struct GetPosts {
/// If true, then show the nsfw posts (even if your user setting is to hide them)
#[cfg_attr(feature = "full", ts(optional))]
pub show_nsfw: Option<bool>,
/// Whether to automatically mark fetched posts as read.
#[cfg_attr(feature = "full", ts(optional))]
pub mark_as_read: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
/// If true, then only show posts with no comments
pub no_comments_only: Option<bool>,
@ -195,17 +198,26 @@ pub struct RemovePost {
#[cfg_attr(feature = "full", ts(export))]
/// Mark a post as read.
pub struct MarkPostAsRead {
pub post_ids: Vec<PostId>,
pub post_id: PostId,
pub read: bool,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Mark several posts as read.
pub struct MarkManyPostsAsRead {
pub post_ids: Vec<PostId>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Hide a post from list views
pub struct HidePost {
pub post_ids: Vec<PostId>,
pub post_id: PostId,
pub hide: bool,
}

View file

@ -18,12 +18,11 @@ use lemmy_db_schema::{
},
};
use lemmy_utils::{
error::{LemmyError, LemmyErrorType, LemmyResult},
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
settings::structs::{PictrsImageMode, Settings},
REQWEST_TIMEOUT,
VERSION,
};
use mime::Mime;
use reqwest::{
header::{CONTENT_TYPE, RANGE},
Client,
@ -61,13 +60,23 @@ pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResu
// server may ignore this and still respond with the full response
.header(RANGE, format!("bytes=0-{}", bytes_to_fetch - 1)) /* -1 because inclusive */
.send()
.await?;
.await?
.error_for_status()?;
let content_type: Option<Mime> = response
.headers()
.get(CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.parse().ok());
// In some cases servers send a wrong mime type for images, which prevents thumbnail
// generation. To avoid this we also try to guess the mime type from file extension.
let content_type = mime_guess::from_path(url.path())
.first()
// If you can guess that its an image type, then return that first.
.filter(|guess| guess.type_() == mime::IMAGE)
// Otherwise, get the content type from the headers
.or(
response
.headers()
.get(CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.parse().ok()),
);
let opengraph_data = {
// if the content type is not text/html, we don't need to parse it
@ -308,7 +317,8 @@ pub async fn purge_image_from_pictrs(image_url: &Url, context: &LemmyContext) ->
.timeout(REQWEST_TIMEOUT)
.header("x-api-token", pictrs_api_key)
.send()
.await?;
.await?
.error_for_status()?;
let response: PictrsPurgeResponse = response.json().await.map_err(LemmyError::from)?;
@ -333,8 +343,8 @@ pub async fn delete_image_from_pictrs(
.delete(&url)
.timeout(REQWEST_TIMEOUT)
.send()
.await
.map_err(LemmyError::from)?;
.await?
.error_for_status()?;
Ok(())
}
@ -366,6 +376,7 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
.timeout(REQWEST_TIMEOUT)
.send()
.await?
.error_for_status()?
.json::<PictrsResponse>()
.await?;
@ -406,16 +417,14 @@ pub async fn fetch_pictrs_proxied_image_details(
// Pictrs needs you to fetch the proxied image before you can fetch the details
let proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}");
let res = context
context
.client()
.get(&proxy_url)
.timeout(REQWEST_TIMEOUT)
.send()
.await?
.status();
if !res.is_success() {
Err(LemmyErrorType::NotAnImageType)?
}
.error_for_status()
.with_lemmy_type(LemmyErrorType::NotAnImageType)?;
let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}");
@ -425,6 +434,7 @@ pub async fn fetch_pictrs_proxied_image_details(
.timeout(REQWEST_TIMEOUT)
.send()
.await?
.error_for_status()?
.json()
.await?;
@ -521,7 +531,7 @@ mod tests {
// root relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='/image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
let metadata = extract_opengraph_data(html_bytes, &url)?;
assert_eq!(
metadata.image,
Some(Url::parse("https://example.com/image.jpg")?.into())
@ -529,7 +539,7 @@ mod tests {
// base relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
let metadata = extract_opengraph_data(html_bytes, &url)?;
assert_eq!(
metadata.image,
Some(Url::parse("https://example.com/one/image.jpg")?.into())
@ -537,7 +547,7 @@ mod tests {
// absolute url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='https://cdn.host.com/image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
let metadata = extract_opengraph_data(html_bytes, &url)?;
assert_eq!(
metadata.image,
Some(Url::parse("https://cdn.host.com/image.jpg")?.into())
@ -545,7 +555,7 @@ mod tests {
// protocol relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='//example.com/image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
let metadata = extract_opengraph_data(html_bytes, &url)?;
assert_eq!(
metadata.image,
Some(Url::parse("https://example.com/image.jpg")?.into())

View file

@ -43,6 +43,8 @@ use lemmy_db_views_actor::structs::{
PersonView,
};
use lemmy_db_views_moderator::structs::{
AdminAllowInstanceView,
AdminBlockInstanceView,
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
@ -183,6 +185,8 @@ pub struct GetModlogResponse {
pub admin_purged_posts: Vec<AdminPurgePostView>,
pub admin_purged_comments: Vec<AdminPurgeCommentView>,
pub hidden_communities: Vec<ModHideCommunityView>,
pub admin_block_instance: Vec<AdminBlockInstanceView>,
pub admin_allow_instance: Vec<AdminAllowInstanceView>,
}
#[skip_serializing_none]
@ -265,10 +269,6 @@ pub struct CreateSite {
#[cfg_attr(feature = "full", ts(optional))]
pub captcha_difficulty: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub allowed_instances: Option<Vec<String>>,
#[cfg_attr(feature = "full", ts(optional))]
pub blocked_instances: Option<Vec<String>>,
#[cfg_attr(feature = "full", ts(optional))]
pub registration_mode: Option<RegistrationMode>,
#[cfg_attr(feature = "full", ts(optional))]
pub oauth_registration: Option<bool>,
@ -394,12 +394,6 @@ pub struct EditSite {
/// The captcha difficulty. Can be easy, medium, or hard
#[cfg_attr(feature = "full", ts(optional))]
pub captcha_difficulty: Option<String>,
/// A list of allowed instances. If none are set, federation is open.
#[cfg_attr(feature = "full", ts(optional))]
pub allowed_instances: Option<Vec<String>>,
/// A list of blocked instances.
#[cfg_attr(feature = "full", ts(optional))]
pub blocked_instances: Option<Vec<String>>,
/// A list of blocked URLs
#[cfg_attr(feature = "full", ts(optional))]
pub blocked_urls: Option<Vec<String>>,
@ -514,6 +508,7 @@ pub struct ReadableFederationState {
next_retry: Option<DateTime<Utc>>,
}
#[allow(clippy::expect_used)]
impl From<FederationQueueState> for ReadableFederationState {
fn from(internal_state: FederationQueueState) -> Self {
ReadableFederationState {
@ -647,15 +642,29 @@ pub struct GetUnreadRegistrationApplicationCountResponse {
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Block an instance as user
pub struct BlockInstance {
pub struct UserBlockInstanceParams {
pub instance_id: InstanceId,
pub block: bool,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct BlockInstanceResponse {
pub blocked: bool,
pub struct AdminBlockInstanceParams {
pub instance: String,
pub block: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub expires: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct AdminAllowInstanceParams {
pub instance: String,
pub allow: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>,
}

View file

@ -23,12 +23,17 @@ use lemmy_db_schema::{
local_site::LocalSite,
local_site_rate_limit::LocalSiteRateLimit,
local_site_url_blocklist::LocalSiteUrlBlocklist,
moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm},
mod_log::moderator::{
ModRemoveComment,
ModRemoveCommentForm,
ModRemovePost,
ModRemovePostForm,
},
oauth_account::OAuthAccount,
password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm},
person_block::PersonBlock,
post::{Post, PostLike, PostRead},
post::{Post, PostLike},
registration_application::RegistrationApplication,
site::Site,
},
@ -60,12 +65,13 @@ use lemmy_utils::{
slurs::{build_slur_regex, remove_slurs},
validation::clean_urls_in_text,
},
CacheLock,
CACHE_DURATION_FEDERATION,
};
use moka::future::Cache;
use regex::{escape, Regex, RegexSet};
use rosetta_i18n::{Language, LanguageId};
use std::{collections::HashSet, sync::LazyLock};
use std::sync::LazyLock;
use tracing::warn;
use url::{ParseError, Url};
use urlencoding::encode;
@ -141,19 +147,6 @@ pub fn is_top_mod(
}
}
/// Marks a post as read for a given person.
#[tracing::instrument(skip_all)]
pub async fn mark_post_as_read(
person_id: PersonId,
post_id: PostId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
PostRead::mark_as_read(pool, HashSet::from([post_id]), person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
Ok(())
}
/// Updates the read comment count for a post. Usually done when reading or creating a new comment.
#[tracing::instrument(skip_all)]
pub async fn update_read_comments(
@ -166,7 +159,6 @@ pub async fn update_read_comments(
person_id,
post_id,
read_comments,
..PersonPostAggregatesForm::default()
};
PersonPostAggregates::upsert(pool, &person_post_agg_form).await?;
@ -456,7 +448,11 @@ pub async fn send_password_reset_email(
// Generate a random token
let token = uuid::Uuid::new_v4().to_string();
let email = &user.local_user.email.clone().expect("email");
let email = &user
.local_user
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language(user);
let subject = &lang.password_reset_subject(&user.person.name);
let protocol_and_hostname = settings.get_protocol_and_hostname();
@ -506,6 +502,7 @@ pub fn get_interface_language_from_settings(user: &LocalUserView) -> Lang {
lang_str_to_lang(&user.local_user.interface_language)
}
#[allow(clippy::expect_used)]
fn lang_str_to_lang(lang: &str) -> Lang {
let lang_id = LanguageId::new(lang);
Lang::from_language_id(&lang_id).unwrap_or_else(|| {
@ -532,11 +529,11 @@ pub fn local_site_rate_limit_to_rate_limit_config(
})
}
pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option<Regex> {
pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option<LemmyResult<Regex>> {
build_slur_regex(local_site.slur_filter_regex.as_deref())
}
pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Regex> {
pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<LemmyResult<Regex>> {
local_site
.as_ref()
.map(local_site_to_slur_regex)
@ -544,7 +541,7 @@ pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Re
}
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
static URL_BLOCKLIST: LazyLock<Cache<(), RegexSet>> = LazyLock::new(|| {
static URL_BLOCKLIST: CacheLock<RegexSet> = LazyLock::new(|| {
Cache::builder()
.max_capacity(1)
.time_to_live(CACHE_DURATION_FEDERATION)
@ -571,7 +568,11 @@ pub async fn send_application_approved_email(
user: &LocalUserView,
settings: &Settings,
) -> LemmyResult<()> {
let email = &user.local_user.email.clone().expect("email");
let email = &user
.local_user
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language(user);
let subject = lang.registration_approved_subject(&user.person.actor_id);
let body = lang.registration_approved_body(&settings.hostname);
@ -593,7 +594,11 @@ pub async fn send_new_applicant_email_to_admins(
);
for admin in &admins {
let email = &admin.local_user.email.clone().expect("email");
let email = &admin
.local_user
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language_from_settings(admin);
let subject = lang.new_application_subject(&settings.hostname, applicant_username);
let body = lang.new_application_body(applications_link);
@ -615,11 +620,13 @@ pub async fn send_new_report_email_to_admins(
let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),);
for admin in &admins {
let email = &admin.local_user.email.clone().expect("email");
let lang = get_interface_language_from_settings(admin);
let subject = lang.new_report_subject(&settings.hostname, reported_username, reporter_username);
let body = lang.new_report_body(reports_link);
send_email(&subject, email, &admin.person.name, &body, settings).await?;
if let Some(email) = &admin.local_user.email {
let lang = get_interface_language_from_settings(admin);
let subject =
lang.new_report_subject(&settings.hostname, reported_username, reporter_username);
let body = lang.new_report_body(reports_link);
send_email(&subject, email, &admin.person.name, &body, settings).await?;
}
}
Ok(())
}
@ -1044,7 +1051,7 @@ pub fn check_conflicting_like_filters(
pub async fn process_markdown(
text: &str,
slur_regex: &Option<Regex>,
slur_regex: &Option<LemmyResult<Regex>>,
url_blocklist: &RegexSet,
context: &LemmyContext,
) -> LemmyResult<String> {
@ -1076,7 +1083,7 @@ pub async fn process_markdown(
pub async fn process_markdown_opt(
text: &Option<String>,
slur_regex: &Option<Regex>,
slur_regex: &Option<LemmyResult<Regex>>,
url_blocklist: &RegexSet,
context: &LemmyContext,
) -> LemmyResult<Option<String>> {

View file

@ -25,7 +25,6 @@ tracing = { workspace = true }
url = { workspace = true }
futures.workspace = true
uuid = { workspace = true }
moka.workspace = true
anyhow.workspace = true
chrono.workspace = true
webmention = "0.6.0"

View file

@ -16,9 +16,8 @@ use lemmy_api_common::{
},
};
use lemmy_db_schema::{
impls::actor_language::default_post_language,
impls::actor_language::validate_post_language,
source::{
actor_language::CommunityLanguage,
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm},
comment_reply::{CommentReply, CommentReplyUpdateForm},
local_site::LocalSite,
@ -93,21 +92,13 @@ pub async fn create_comment(
check_comment_depth(parent)?;
}
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => lid,
None => {
default_post_language(
&mut context.pool(),
community_id,
local_user_view.local_user.id,
)
.await?
}
};
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id)
.await?;
let language_id = validate_post_language(
&mut context.pool(),
data.language_id,
community_id,
local_user_view.local_user.id,
)
.await?;
let comment_form = CommentInsertForm {
language_id: Some(language_id),

View file

@ -12,7 +12,7 @@ use lemmy_db_schema::{
comment::{Comment, CommentUpdateForm},
comment_report::CommentReport,
local_user::LocalUser,
moderator::{ModRemoveComment, ModRemoveCommentForm},
mod_log::moderator::{ModRemoveComment, ModRemoveCommentForm},
},
traits::{Crud, Reportable},
};

View file

@ -1,5 +1,6 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
build_response::{build_comment_response, send_local_notifs},
comment::{CommentResponse, EditComment},
@ -13,13 +14,12 @@ use lemmy_api_common::{
},
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
source::{
actor_language::CommunityLanguage,
comment::{Comment, CommentUpdateForm},
local_site::LocalSite,
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::{
@ -55,14 +55,13 @@ pub async fn update_comment(
Err(LemmyErrorType::NoCommentEditAllowed)?
}
if let Some(language_id) = data.language_id {
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_comment.community.id,
)
.await?;
}
let language_id = validate_post_language(
&mut context.pool(),
data.language_id,
orig_comment.community.id,
local_user_view.local_user.id,
)
.await?;
let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?;
@ -74,8 +73,8 @@ pub async fn update_comment(
let comment_id = data.comment_id;
let form = CommentUpdateForm {
content,
language_id: data.language_id,
updated: Some(Some(naive_now())),
language_id: Some(language_id),
updated: Some(Some(Utc::now())),
..Default::default()
};
let updated_comment = Comment::update(&mut context.pool(), comment_id, &form)

View file

@ -10,7 +10,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
community::{Community, CommunityUpdateForm},
moderator::{ModRemoveCommunity, ModRemoveCommunityForm},
mod_log::moderator::{ModRemoveCommunity, ModRemoveCommunityForm},
},
traits::Crud,
};

View file

@ -1,6 +1,7 @@
use super::check_community_visibility_allowed;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
build_response::build_community_response,
community::{CommunityResponse, EditCommunity},
@ -22,7 +23,7 @@ use lemmy_db_schema::{
local_site::LocalSite,
},
traits::Crud,
utils::{diesel_string_update, diesel_url_update, naive_now},
utils::{diesel_string_update, diesel_url_update},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{
@ -95,7 +96,7 @@ pub async fn update_community(
nsfw: data.nsfw,
posting_restricted_to_mods: data.posting_restricted_to_mods,
visibility: data.visibility,
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
..Default::default()
};

View file

@ -1,10 +1,11 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{context::LemmyContext, oauth_provider::EditOAuthProvider, utils::is_admin};
use lemmy_db_schema::{
source::oauth_provider::{OAuthProvider, OAuthProviderUpdateForm},
traits::Crud,
utils::{diesel_required_string_update, diesel_required_url_update, naive_now},
utils::{diesel_required_string_update, diesel_required_url_update},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
@ -32,7 +33,7 @@ pub async fn update_oauth_provider(
auto_verify_email: data.auto_verify_email,
account_linking_enabled: data.account_linking_enabled,
enabled: data.enabled,
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
};
let update_result =

View file

@ -12,17 +12,15 @@ use lemmy_api_common::{
get_url_blocklist,
honeypot_check,
local_site_to_slur_regex,
mark_post_as_read,
process_markdown_opt,
},
};
use lemmy_db_schema::{
impls::actor_language::default_post_language,
impls::actor_language::validate_post_language,
source::{
actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite,
post::{Post, PostInsertForm, PostLike, PostLikeForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostReadForm},
},
traits::{Crud, Likeable},
utils::diesel_url_create,
@ -98,23 +96,13 @@ pub async fn create_post(
.await?;
}
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => lid,
None => {
default_post_language(
&mut context.pool(),
community.id,
local_user_view.local_user.id,
)
.await?
}
};
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community.id)
.await?;
let language_id = validate_post_language(
&mut context.pool(),
data.language_id,
data.community_id,
local_user_view.local_user.id,
)
.await?;
let scheduled_publish_time =
convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?;
@ -154,17 +142,14 @@ pub async fn create_post(
// They like their own post by default
let person_id = local_user_view.person.id;
let post_id = inserted_post.id;
let like_form = PostLikeForm {
post_id,
person_id,
score: 1,
};
let like_form = PostLikeForm::new(post_id, person_id, 1);
PostLike::like(&mut context.pool(), &like_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
build_post_response(&context, community_id, local_user_view, post_id).await
}

View file

@ -2,10 +2,13 @@ use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{GetPost, GetPostResponse},
utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read, update_read_comments},
utils::{check_private_instance, is_mod_or_admin_opt, update_read_comments},
};
use lemmy_db_schema::{
source::{comment::Comment, post::Post},
source::{
comment::Comment,
post::{Post, PostRead, PostReadForm},
},
traits::Crud,
};
use lemmy_db_views::{
@ -62,7 +65,8 @@ pub async fn get_post(
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?;
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
update_read_comments(
person_id,

View file

@ -11,7 +11,7 @@ use lemmy_db_schema::{
source::{
community::Community,
local_user::LocalUser,
moderator::{ModRemovePost, ModRemovePostForm},
mod_log::moderator::{ModRemovePost, ModRemovePostForm},
post::{Post, PostUpdateForm},
post_report::PostReport,
},

View file

@ -1,6 +1,7 @@
use super::{convert_published_time, create::send_webmention};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
build_response::build_post_response,
context::LemmyContext,
@ -15,14 +16,14 @@ use lemmy_api_common::{
},
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
source::{
actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite,
post::{Post, PostUpdateForm},
},
traits::Crud,
utils::{diesel_string_update, diesel_url_update, naive_now},
utils::{diesel_string_update, diesel_url_update},
};
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{
@ -101,14 +102,13 @@ pub async fn update_post(
Err(LemmyErrorType::NoPostEditAllowed)?
}
if let Some(language_id) = data.language_id {
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_post.community.id,
)
.await?;
}
let language_id = validate_post_language(
&mut context.pool(),
data.language_id,
orig_post.post.community_id,
local_user_view.local_user.id,
)
.await?;
// handle changes to scheduled_publish_time
let scheduled_publish_time = match (
@ -131,8 +131,8 @@ pub async fn update_post(
body,
alt_text,
nsfw: data.nsfw,
language_id: data.language_id,
updated: Some(Some(naive_now())),
language_id: Some(language_id),
updated: Some(Some(Utc::now())),
scheduled_publish_time,
..Default::default()
};

View file

@ -1,5 +1,6 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
private_message::{EditPrivateMessage, PrivateMessageResponse},
@ -12,7 +13,6 @@ use lemmy_db_schema::{
private_message::{PrivateMessage, PrivateMessageUpdateForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
use lemmy_utils::{
@ -47,7 +47,7 @@ pub async fn update_private_message(
private_message_id,
&PrivateMessageUpdateForm {
content: Some(content),
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
..Default::default()
},
)

View file

@ -2,6 +2,7 @@ use super::not_zero;
use crate::site::{application_question_check, site_default_post_listing_type_check};
use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
site::{CreateSite, SiteResponse},
@ -23,7 +24,7 @@ use lemmy_db_schema::{
site::{Site, SiteUpdateForm},
},
traits::Crud,
utils::{diesel_string_update, diesel_url_create, naive_now},
utils::{diesel_string_update, diesel_url_create},
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{
@ -75,7 +76,7 @@ pub async fn create_site(
icon: Some(icon),
banner: Some(banner),
actor_id: Some(actor_id),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
inbox_url,
private_key: Some(Some(keypair.private_key)),
public_key: Some(keypair.public_key),
@ -102,7 +103,7 @@ pub async fn create_site(
legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
actor_name_max_length: data.actor_name_max_length,
federation_enabled: data.federation_enabled,
@ -161,7 +162,7 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
.slur_filter_regex
.as_deref()
.or(local_site.slur_filter_regex.as_deref()),
)?;
);
site_name_length_check(&create_site.name)?;
check_slurs(&create_site.name, &slur_regex)?;

View file

@ -16,11 +16,11 @@ use lemmy_db_schema::source::{
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView};
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
CACHE_DURATION_API,
build_cache,
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
CacheLock,
VERSION,
};
use moka::future::Cache;
use std::sync::LazyLock;
#[tracing::instrument(skip(context))]
@ -28,41 +28,10 @@ pub async fn get_site(
local_user_view: Option<LocalUserView>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<GetSiteResponse>> {
static CACHE: LazyLock<Cache<(), GetSiteResponse>> = LazyLock::new(|| {
Cache::builder()
.max_capacity(1)
.time_to_live(CACHE_DURATION_API)
.build()
});
// This data is independent from the user account so we can cache it across requests
static CACHE: CacheLock<GetSiteResponse> = LazyLock::new(build_cache);
let mut site_response = CACHE
.try_get_with::<_, LemmyError>((), async {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?;
let oauth_providers =
OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone());
Ok(GetSiteResponse {
site_view,
admins,
version: VERSION.to_string(),
my_user: None,
all_languages,
discussion_languages,
blocked_urls,
tagline,
oauth_providers: Some(oauth_providers),
admin_oauth_providers: Some(admin_oauth_providers),
taglines: vec![],
custom_emojis: vec![],
})
})
.try_get_with((), read_site(&context))
.await
.map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?;
@ -112,3 +81,29 @@ pub async fn get_site(
Ok(Json(site_response))
}
async fn read_site(context: &LemmyContext) -> LemmyResult<GetSiteResponse> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?;
let oauth_providers = OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone());
Ok(GetSiteResponse {
site_view,
admins,
version: VERSION.to_string(),
my_user: None,
all_languages,
discussion_languages,
blocked_urls,
tagline,
oauth_providers: Some(oauth_providers),
admin_oauth_providers: Some(admin_oauth_providers),
taglines: vec![],
custom_emojis: vec![],
})
}

View file

@ -2,6 +2,7 @@ use super::not_zero;
use crate::site::{application_question_check, site_default_post_listing_type_check};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
request::replace_image,
@ -18,8 +19,6 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
actor_language::SiteLanguage,
federation_allowlist::FederationAllowList,
federation_blocklist::FederationBlockList,
local_site::{LocalSite, LocalSiteUpdateForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
local_site_url_blocklist::LocalSiteUrlBlocklist,
@ -27,7 +26,7 @@ use lemmy_db_schema::{
site::{Site, SiteUpdateForm},
},
traits::Crud,
utils::{diesel_string_update, diesel_url_update, naive_now},
utils::{diesel_string_update, diesel_url_update},
RegistrationMode,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
@ -88,7 +87,7 @@ pub async fn update_site(
icon,
banner,
content_warning: diesel_string_update(data.content_warning.as_deref()),
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
..Default::default()
};
@ -111,7 +110,7 @@ pub async fn update_site(
legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
actor_name_max_length: data.actor_name_max_length,
federation_enabled: data.federation_enabled,
@ -151,12 +150,6 @@ pub async fn update_site(
.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?;
if let Some(url_blocklist) = data.blocked_urls.clone() {
let parsed_urls = check_urls_are_valid(&url_blocklist)?;
LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?;
@ -210,7 +203,7 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm
.slur_filter_regex
.as_deref()
.or(local_site.slur_filter_regex.as_deref()),
)?;
);
if let Some(name) = &edit_site.name {
// The name doesn't need to be updated, but if provided it cannot be blanked out...

View file

@ -1,5 +1,6 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
tagline::{TaglineResponse, UpdateTagline},
@ -11,7 +12,6 @@ use lemmy_db_schema::{
tagline::{Tagline, TaglineUpdateForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
@ -33,7 +33,7 @@ pub async fn update_tagline(
let tagline_form = TaglineUpdateForm {
content,
updated: naive_now(),
updated: Utc::now(),
};
let tagline = Tagline::update(&mut context.pool(), data.id, &tagline_form).await?;

View file

@ -21,8 +21,9 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
aggregates::structs::PersonAggregates,
newtypes::{InstanceId, OAuthProviderId},
newtypes::{InstanceId, OAuthProviderId, SiteId},
source::{
actor_language::SiteLanguage,
captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer},
language::Language,
local_site::LocalSite,
@ -145,17 +146,24 @@ pub async fn register(
..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string()))
};
let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?;
let inserted_local_user = create_local_user(
&context,
language_tags,
&local_user_form,
local_site.site_id,
)
.await?;
if local_site.site_setup && require_registration_application {
// Create the registration application
let form = RegistrationApplicationInsertForm {
local_user_id: inserted_local_user.id,
// We already made sure answer was not null above
answer: data.answer.clone().expect("must have an answer"),
};
if let Some(answer) = data.answer.clone() {
// Create the registration application
let form = RegistrationApplicationInsertForm {
local_user_id: inserted_local_user.id,
answer,
};
RegistrationApplication::create(&mut context.pool(), &form).await?;
RegistrationApplication::create(&mut context.pool(), &form).await?;
}
}
// Email the admins, only if email verification is not required
@ -304,7 +312,7 @@ pub async fn authenticate_with_oauth(
OAuthAccount::create(&mut context.pool(), &oauth_account_form)
.await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
local_user = user_view.local_user.clone();
} else {
@ -357,7 +365,13 @@ pub async fn authenticate_with_oauth(
..LocalUserInsertForm::new(person.id, None)
};
local_user = create_local_user(&context, language_tags, &local_user_form).await?;
local_user = create_local_user(
&context,
language_tags,
&local_user_form,
local_site.site_id,
)
.await?;
// Create the oauth account
let oauth_account_form =
@ -365,7 +379,7 @@ pub async fn authenticate_with_oauth(
OAuthAccount::create(&mut context.pool(), &oauth_account_form)
.await
.map_err(|_| LemmyErrorType::IncorrectLogin)?;
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
// prevent sign in until application is accepted
if local_site.site_setup
@ -373,17 +387,19 @@ pub async fn authenticate_with_oauth(
&& !local_user.accepted_application
&& !local_user.admin
{
// Create the registration application
RegistrationApplication::create(
&mut context.pool(),
&RegistrationApplicationInsertForm {
local_user_id: local_user.id,
answer: data.answer.clone().expect("must have an answer"),
},
)
.await?;
if let Some(answer) = data.answer.clone() {
// Create the registration application
RegistrationApplication::create(
&mut context.pool(),
&RegistrationApplicationInsertForm {
local_user_id: local_user.id,
answer,
},
)
.await?;
login_response.registration_created = true;
login_response.registration_created = true;
}
}
// Check email is verified when required
@ -446,15 +462,23 @@ async fn create_local_user(
context: &Data<LemmyContext>,
language_tags: Vec<String>,
local_user_form: &LocalUserInsertForm,
local_site_id: SiteId,
) -> Result<LocalUser, LemmyError> {
let all_languages = Language::read_all(&mut context.pool()).await?;
// use hashset to avoid duplicates
let mut language_ids = HashSet::new();
// Enable languages from `Accept-Language` header
for l in language_tags {
if let Some(found) = all_languages.iter().find(|all| all.code == l) {
language_ids.insert(found.id);
}
}
// Enable site languages. Ignored if all languages are enabled.
let discussion_languages = SiteLanguage::read(&mut context.pool(), local_site_id).await?;
language_ids.extend(discussion_languages);
let language_ids = language_ids.into_iter().collect();
let inserted_local_user =
@ -483,7 +507,7 @@ async fn send_verification_email_if_required(
&local_user
.email
.clone()
.expect("invalid verification email"),
.ok_or(LemmyErrorType::EmailRequired)?,
&mut context.pool(),
context.settings(),
)
@ -524,18 +548,16 @@ async fn oauth_request_access_token(
("client_secret", &oauth_provider.client_secret),
])
.send()
.await;
let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
if !response.status().is_success() {
Err(LemmyErrorType::OauthLoginFailed)?;
}
.await
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?
.error_for_status()
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
// Extract the access token
let token_response = response
.json::<TokenResponse>()
.await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
Ok(token_response)
}
@ -552,18 +574,16 @@ async fn oidc_get_user_info(
.header("Accept", "application/json")
.bearer_auth(access_token)
.send()
.await;
let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
if !response.status().is_success() {
Err(LemmyErrorType::OauthLoginFailed)?;
}
.await
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?
.error_for_status()
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
// Extract the OAUTH user_id claim from the returned user_info
let user_info = response
.json::<serde_json::Value>()
.await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
Ok(user_info)
}
@ -571,7 +591,7 @@ async fn oidc_get_user_info(
fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult<String> {
if let Some(value) = user_info.get(key) {
let result = serde_json::from_value::<String>(value.clone())
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
return Ok(result);
}
Err(LemmyErrorType::OauthLoginFailed)?

View file

@ -42,7 +42,7 @@ reqwest = { workspace = true }
moka.workspace = true
serde_with.workspace = true
html2md = "0.2.14"
html2text = "0.12.5"
html2text = "0.12.6"
stringreader = "0.1.1"
enum_delegate = "0.2.0"

View file

@ -36,7 +36,7 @@ use lemmy_db_schema::{
CommunityPersonBan,
CommunityPersonBanForm,
},
moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm},
mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm},
person::{Person, PersonUpdateForm},
},
traits::{Bannable, Crud, Followable},

View file

@ -27,7 +27,7 @@ use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
community::{CommunityPersonBan, CommunityPersonBanForm},
moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm},
mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm},
person::{Person, PersonUpdateForm},
},
traits::{Bannable, Crud},

View file

@ -130,7 +130,7 @@ impl AnnounceActivity {
actor: c.actor.clone().into_inner(),
other: serde_json::to_value(c.object)?
.as_object()
.expect("is object")
.ok_or(FederationError::Unreachable)?
.clone(),
};
let announce_compat = AnnounceActivity::new(announcable_page, community, context)?;
@ -215,7 +215,7 @@ async fn can_accept_activity_in_community(
) -> LemmyResult<()> {
if let Some(community) = community {
// Local only community can't federate
if community.visibility != CommunityVisibility::Public {
if community.visibility == CommunityVisibility::LocalOnly {
return Err(LemmyErrorType::NotFound.into());
}
if !community.local {

View file

@ -31,7 +31,7 @@ use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModAddCommunity, ModAddCommunityForm},
mod_log::moderator::{ModAddCommunity, ModAddCommunityForm},
person::Person,
post::{Post, PostUpdateForm},
},

View file

@ -27,7 +27,7 @@ use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModAddCommunity, ModAddCommunityForm},
mod_log::moderator::{ModAddCommunity, ModAddCommunityForm},
post::{Post, PostUpdateForm},
},
traits::{Crud, Joinable},

View file

@ -27,7 +27,7 @@ use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
community::Community,
moderator::{ModLockPost, ModLockPostForm},
mod_log::moderator::{ModLockPost, ModLockPostForm},
person::Person,
post::{Post, PostUpdateForm},
},

View file

@ -42,7 +42,7 @@ pub(crate) async fn send_activity_in_community(
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
// If community is local only, don't send anything out
if community.visibility != CommunityVisibility::Public {
if community.visibility == CommunityVisibility::LocalOnly {
return Ok(());
}

View file

@ -70,7 +70,8 @@ impl Report {
let object_creator = Person::read(&mut context.pool(), object_creator_id).await?;
let object_creator_site: Option<ApubSite> =
Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id)
.await?
.await
.ok()
.map(Into::into);
if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) {
inboxes.add_inbox(inbox);

View file

@ -17,6 +17,7 @@ use activitypub_federation::{
kinds::activity::UpdateType,
traits::{ActivityHandler, Actor, Object},
};
use chrono::Utc;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
@ -25,7 +26,6 @@ use lemmy_db_schema::{
person::Person,
},
traits::Crud,
utils::naive_now,
};
use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url;
@ -103,7 +103,7 @@ impl ActivityHandler for UpdateCommunity {
nsfw: Some(self.object.sensitive.unwrap_or(false)),
actor_id: Some(self.object.id.into()),
public_key: Some(self.object.public_key.public_key_pem),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
icon: Some(self.object.icon.map(|i| i.url.into())),
banner: Some(self.object.image.map(|i| i.url.into())),
followers_url: self.object.followers.map(Into::into),

View file

@ -171,6 +171,9 @@ impl ActivityHandler for CreateOrUpdateNote {
// TODO: for compatibility with other projects, it would be much better to read this from cc or
// tags
let mentions = scrape_text_for_mentions(&comment.content);
// TODO: this fails in local community comment as CommentView::read() returns nothing
// without passing LocalUser
send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?;
Ok(())
}

View file

@ -118,11 +118,7 @@ impl ActivityHandler for CreateOrUpdatePage {
let post = ApubPost::from_json(self.object, context).await?;
// author likes their own post by default
let like_form = PostLikeForm {
post_id: post.id,
person_id: post.creator_id,
score: 1,
};
let like_form = PostLikeForm::new(post.id, post.creator_id, 1);
PostLike::like(&mut context.pool(), &like_form).await?;
// Calculate initial hot_rank for post

View file

@ -14,7 +14,7 @@ use lemmy_db_schema::{
comment::{Comment, CommentUpdateForm},
comment_report::CommentReport,
community::{Community, CommunityUpdateForm},
moderator::{
mod_log::moderator::{
ModRemoveComment,
ModRemoveCommentForm,
ModRemoveCommunity,

View file

@ -13,7 +13,7 @@ use lemmy_db_schema::{
source::{
comment::{Comment, CommentUpdateForm},
community::{Community, CommunityUpdateForm},
moderator::{
mod_log::moderator::{
ModRemoveComment,
ModRemoveCommentForm,
ModRemoveCommunity,

View file

@ -79,11 +79,7 @@ async fn vote_post(
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let post_id = post.id;
let like_form = PostLikeForm {
post_id: post.id,
person_id: actor.id,
score: vote_type.into(),
};
let like_form = PostLikeForm::new(post.id, actor.id, vote_type.into());
let person_id = actor.id;
PostLike::remove(&mut context.pool(), person_id, post_id).await?;
PostLike::like(&mut context.pool(), &like_form).await?;

View file

@ -10,7 +10,10 @@ use lemmy_api_common::{
post::{GetPosts, GetPostsResponse},
utils::{check_conflicting_like_filters, check_private_instance},
};
use lemmy_db_schema::source::community::Community;
use lemmy_db_schema::{
newtypes::PostId,
source::{community::Community, post::PostRead},
};
use lemmy_db_views::{
post_view::PostQuery,
structs::{LocalUserView, PaginationCursor, SiteView},
@ -90,6 +93,17 @@ pub async fn list_posts(
.await
.with_lemmy_type(LemmyErrorType::CouldntGetPosts)?;
// If in their user settings (or as part of the API request), auto-mark fetched posts as read
if let Some(local_user) = local_user {
if data
.mark_as_read
.unwrap_or(local_user.auto_mark_fetched_posts_as_read)
{
let post_ids = posts.iter().map(|p| p.post.id).collect::<Vec<PostId>>();
PostRead::mark_many_as_read(&mut context.pool(), &post_ids, local_user.person_id).await?;
}
}
// if this page wasn't empty, then there is a next page after the last post on this page
let next_page = posts.last().map(PaginationCursor::after_post);
Ok(Json(GetPostsResponse { posts, next_page }))

View file

@ -200,10 +200,7 @@ pub async fn import_settings(
&context,
|(saved, context)| async move {
let post = saved.dereference(&context).await?;
let form = PostSavedForm {
person_id,
post_id: post.id,
};
let form = PostSavedForm::new(post.id, person_id);
PostSaved::save(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
},
@ -325,7 +322,7 @@ pub(crate) mod tests {
CommunityFollowerState,
CommunityInsertForm,
},
local_user::LocalUser,
person::Person,
},
traits::{Crud, Followable},
};
@ -379,8 +376,8 @@ pub(crate) mod tests {
assert_eq!(follows.len(), 1);
assert_eq!(follows[0].community.actor_id, community.actor_id);
LocalUser::delete(pool, export_user.local_user.id).await?;
LocalUser::delete(pool, import_user.local_user.id).await?;
Person::delete(pool, export_user.person.id).await?;
Person::delete(pool, import_user.person.id).await?;
Ok(())
}
@ -415,8 +412,8 @@ pub(crate) mod tests {
Some(LemmyErrorType::TooManyItems)
);
LocalUser::delete(pool, export_user.local_user.id).await?;
LocalUser::delete(pool, import_user.local_user.id).await?;
Person::delete(pool, export_user.person.id).await?;
Person::delete(pool, import_user.person.id).await?;
Ok(())
}

View file

@ -5,7 +5,7 @@ use activitypub_federation::{
};
use diesel::NotFound;
use itertools::Itertools;
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{context::LemmyContext, LemmyErrorType};
use lemmy_db_schema::traits::ApubActor;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyResult};
@ -42,7 +42,7 @@ where
let (name, domain) = identifier
.splitn(2, '@')
.collect_tuple()
.expect("invalid query");
.ok_or(LemmyErrorType::InvalidUrl)?;
let actor = DbActor::read_from_name_and_domain(&mut context.pool(), name, domain)
.await
.ok()

View file

@ -6,28 +6,41 @@ use crate::{
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
},
fetcher::site_or_community_or_user::SiteOrCommunityOrUser,
http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response},
objects::community::ApubCommunity,
};
use activitypub_federation::{
actix_web::signing_actor,
config::Data,
fetch::object_id::ObjectId,
traits::{Collection, Object},
};
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::{
web::{Path, Query},
HttpRequest,
HttpResponse,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
use lemmy_db_schema::{source::community::Community, traits::ApubActor, CommunityVisibility};
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use serde::Deserialize;
#[derive(Deserialize, Clone)]
pub(crate) struct CommunityQuery {
pub(crate) struct CommunityPath {
community_name: String,
}
#[derive(Deserialize, Clone)]
pub struct CommunityIsFollowerQuery {
is_follower: Option<ObjectId<SiteOrCommunityOrUser>>,
}
/// Return the ActivityPub json representation of a local community over HTTP.
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_community_http(
info: web::Path<CommunityQuery>,
info: Path<CommunityPath>,
context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> {
let community: ApubCommunity =
@ -47,21 +60,59 @@ pub(crate) async fn get_apub_community_http(
/// Returns an empty followers collection, only populating the size (for privacy).
pub(crate) async fn get_apub_community_followers(
info: web::Path<CommunityQuery>,
info: Path<CommunityPath>,
query: Query<CommunityIsFollowerQuery>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
let community = Community::read_from_name(&mut context.pool(), &info.community_name, false)
.await?
.ok_or(LemmyErrorType::NotFound)?;
if let Some(is_follower) = &query.is_follower {
return check_is_follower(community, is_follower, context, request).await;
}
check_community_fetchable(&community)?;
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
create_apub_response(&followers)
}
/// Checks if a given actor follows the private community. Returns status 200 if true.
async fn check_is_follower(
community: Community,
is_follower: &ObjectId<SiteOrCommunityOrUser>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
if community.visibility != CommunityVisibility::Private {
return Ok(HttpResponse::BadRequest().body("must be a private community"));
}
// also check for http sig so that followers are not exposed publicly
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(&request, None, &context).await?;
CommunityFollowerView::check_has_followers_from_instance(
community.id,
signing_actor.instance_id(),
&mut context.pool(),
)
.await?;
let instance_id = is_follower.dereference(&context).await?.instance_id();
let has_followers = CommunityFollowerView::check_has_followers_from_instance(
community.id,
instance_id,
&mut context.pool(),
)
.await;
if has_followers.is_ok() {
Ok(HttpResponse::Ok().finish())
} else {
Ok(HttpResponse::NotFound().finish())
}
}
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
/// activities like votes or comments).
pub(crate) async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>,
info: Path<CommunityPath>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
@ -77,7 +128,7 @@ pub(crate) async fn get_apub_community_outbox(
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_community_moderators(
info: web::Path<CommunityQuery>,
info: Path<CommunityPath>,
context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> {
let community: ApubCommunity =
@ -92,7 +143,7 @@ pub(crate) async fn get_apub_community_moderators(
/// Returns collection of featured (stickied) posts.
pub(crate) async fn get_apub_community_featured(
info: web::Path<CommunityQuery>,
info: Path<CommunityPath>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
@ -181,17 +232,17 @@ pub(crate) mod tests {
let request = TestRequest::default().to_http_request();
// fetch invalid community
let query = CommunityQuery {
let query = CommunityPath {
community_name: "asd".to_string(),
};
let res = get_apub_community_http(query.into(), context.reset_request_count()).await;
assert!(res.is_err());
// fetch valid community
let query = CommunityQuery {
let path = CommunityPath {
community_name: community.name.clone(),
};
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?;
let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
let res_group: Group = decode_response(res).await?;
let community: ApubCommunity = community.into();
@ -199,20 +250,26 @@ pub(crate) mod tests {
assert_eq!(group, res_group);
let res = get_apub_community_featured(
query.clone().into(),
path.clone().into(),
context.reset_request_count(),
request.clone(),
)
.await?;
assert_eq!(200, res.status());
let query = Query(CommunityIsFollowerQuery { is_follower: None });
let res = get_apub_community_followers(
path.clone().into(),
query,
context.reset_request_count(),
request.clone(),
)
.await?;
assert_eq!(200, res.status());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await?;
get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
let res =
get_apub_community_outbox(query.into(), context.reset_request_count(), request).await?;
get_apub_community_outbox(path.into(), context.reset_request_count(), request).await?;
assert_eq!(200, res.status());
Instance::delete(&mut context.pool(), instance.id).await?;
@ -227,28 +284,35 @@ pub(crate) mod tests {
let request = TestRequest::default().to_http_request();
// should return tombstone
let query = CommunityQuery {
let path: Path<CommunityPath> = CommunityPath {
community_name: community.name.clone(),
};
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?;
}
.into();
let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await?;
assert_eq!(410, res.status());
let res_tombstone = decode_response::<Tombstone>(res).await;
assert!(res_tombstone.is_ok());
let res = get_apub_community_featured(
query.clone().into(),
path.clone().into(),
context.reset_request_count(),
request.clone(),
)
.await;
assert!(res.is_err());
let query = Query(CommunityIsFollowerQuery { is_follower: None });
let res = get_apub_community_followers(
path.clone().into(),
query,
context.reset_request_count(),
request.clone(),
)
.await;
assert!(res.is_err());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await;
let res = get_apub_community_outbox(path, context.reset_request_count(), request).await;
assert!(res.is_err());
//Community::delete(&mut context.pool(), community.id).await?;
@ -263,25 +327,32 @@ pub(crate) mod tests {
let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?;
let request = TestRequest::default().to_http_request();
let query = CommunityQuery {
let path: Path<CommunityPath> = CommunityPath {
community_name: community.name.clone(),
};
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await;
}
.into();
let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res = get_apub_community_featured(
query.clone().into(),
path.clone().into(),
context.reset_request_count(),
request.clone(),
)
.await;
assert!(res.is_err());
let query = Query(CommunityIsFollowerQuery { is_follower: None });
let res = get_apub_community_followers(
path.clone().into(),
query,
context.reset_request_count(),
request.clone(),
)
.await;
assert!(res.is_err());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await;
let res = get_apub_community_outbox(path, context.reset_request_count(), request).await;
assert!(res.is_err());
Instance::delete(&mut context.pool(), instance.id).await?;

View file

@ -8,6 +8,7 @@ use activitypub_federation::{
actix_web::{inbox::receive_activity, signing_actor},
config::Data,
protocol::context::WithContext,
traits::Actor,
FEDERATION_CONTENT_TYPE,
};
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
@ -18,7 +19,7 @@ use lemmy_db_schema::{
CommunityVisibility,
};
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::{FederationError, LemmyErrorType, LemmyResult};
use lemmy_utils::error::{FederationError, LemmyErrorExt, LemmyErrorType, LemmyResult};
use serde::{Deserialize, Serialize};
use std::{ops::Deref, time::Duration};
use tokio::time::timeout;
@ -46,7 +47,7 @@ pub async fn shared_inbox(
// consider the activity broken and move on.
timeout(INCOMING_ACTIVITY_TIMEOUT, receive_fut)
.await
.map_err(|_| FederationError::InboxTimeout)?
.with_lemmy_type(FederationError::InboxTimeout.into())?
}
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
@ -109,7 +110,7 @@ pub(crate) async fn get_activity(
.into();
let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id)
.await
.map_err(|_| FederationError::CouldntFindActivity)?;
.with_lemmy_type(FederationError::CouldntFindActivity.into())?;
let sensitive = activity.sensitive;
if sensitive {
@ -145,14 +146,27 @@ async fn check_community_content_fetchable(
// from the fetching instance then fetching is allowed
Private => {
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(request, None, context).await?;
Ok(
CommunityFollowerView::check_has_followers_from_instance(
community.id,
signing_actor.instance_id(),
&mut context.pool(),
if community.local {
Ok(
CommunityFollowerView::check_has_followers_from_instance(
community.id,
signing_actor.instance_id(),
&mut context.pool(),
)
.await?,
)
.await?,
)
} else if let Some(followers_url) = community.followers_url.clone() {
let mut followers_url = followers_url.inner().clone();
followers_url
.query_pairs_mut()
.append_pair("is_follower", signing_actor.id().as_str());
let req = context.client().get(followers_url.as_str());
let req = context.sign_request(req, Bytes::new()).await?;
context.client().execute(req).await?.error_for_status()?;
Ok(())
} else {
Err(LemmyErrorType::NotFound.into())
}
}
}
}

View file

@ -11,6 +11,7 @@ use lemmy_db_schema::{
};
use lemmy_utils::{
error::{FederationError, LemmyError, LemmyErrorType, LemmyResult},
CacheLock,
CACHE_DURATION_FEDERATION,
};
use moka::future::Cache;
@ -50,7 +51,8 @@ impl UrlVerifier for VerifyUrlData {
async fn verify(&self, url: &Url) -> Result<(), ActivityPubError> {
let local_site_data = local_site_data_cached(&mut (&self.0).into())
.await
.expect("read local site data");
.map_err(|e| ActivityPubError::Other(format!("Cant read local site data: {e}")))?;
use FederationError::*;
check_apub_id_valid(url, &local_site_data).map_err(|err| match err {
LemmyError {
@ -138,7 +140,7 @@ pub(crate) async fn local_site_data_cached(
// multiple times. This causes a huge number of database reads if we hit the db directly. So we
// cache these values for a short time, which will already make a huge difference and ensures that
// changes take effect quickly.
static CACHE: LazyLock<Cache<(), Arc<LocalSiteData>>> = LazyLock::new(|| {
static CACHE: CacheLock<Arc<LocalSiteData>> = LazyLock::new(|| {
Cache::builder()
.max_capacity(1)
.time_to_live(CACHE_DURATION_FEDERATION)
@ -176,10 +178,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
.domain()
.ok_or(FederationError::UrlWithoutDomain)?
.to_string();
let local_instance = context
.settings()
.get_hostname_without_port()
.expect("local hostname is valid");
let local_instance = context.settings().get_hostname_without_port()?;
if domain == local_instance {
return Ok(());
}
@ -196,10 +195,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
.iter()
.map(|i| i.domain.clone())
.collect::<Vec<String>>();
let local_instance = context
.settings()
.get_hostname_without_port()
.expect("local hostname is valid");
let local_instance = context.settings().get_hostname_without_port()?;
allowed_and_local.push(local_instance);
let domain = apub_id

View file

@ -30,7 +30,6 @@ use lemmy_db_schema::{
post::Post,
},
traits::Crud,
utils::naive_now,
};
use lemmy_utils::{
error::{FederationError, LemmyError, LemmyResult},
@ -204,7 +203,7 @@ impl Object for ApubComment {
language_id,
};
let parent_comment_path = parent_comment.map(|t| t.0.path);
let timestamp: DateTime<Utc> = note.updated.or(note.published).unwrap_or_else(naive_now);
let timestamp: DateTime<Utc> = note.updated.or(note.published).unwrap_or_else(Utc::now);
let comment = Comment::insert_apub(
&mut context.pool(),
Some(timestamp),

View file

@ -38,7 +38,6 @@ use lemmy_db_schema::{
local_site::LocalSite,
},
traits::{ApubActor, Crud},
utils::naive_now,
CommunityVisibility,
};
use lemmy_db_views_actor::structs::CommunityFollowerView;
@ -166,7 +165,7 @@ impl Object for ApubCommunity {
nsfw: Some(group.sensitive.unwrap_or(false)),
actor_id: Some(group.id.into()),
local: Some(false),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
icon,
banner,
sidebar,
@ -193,11 +192,17 @@ impl Object for ApubCommunity {
let languages =
LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).await?;
let timestamp = group.updated.or(group.published).unwrap_or_else(naive_now);
let community = Community::insert_apub(&mut context.pool(), timestamp, &form).await?;
let timestamp = group.updated.or(group.published).unwrap_or_else(Utc::now);
let community: ApubCommunity = Community::insert_apub(&mut context.pool(), timestamp, &form)
.await?
.into();
CommunityLanguage::update(&mut context.pool(), languages, community.id).await?;
let community: ApubCommunity = community.into();
// Need to fetch mods synchronously, otherwise fetching a post in community with
// `posting_restricted_to_mods` can fail if mods havent been fetched yet.
if let Some(moderators) = group.attributed_to {
moderators.dereference(&community, context).await.ok();
}
// These collections are not necessary for Lemmy to work, so ignore errors.
let community_ = community.clone();
@ -210,9 +215,6 @@ impl Object for ApubCommunity {
if let Some(featured) = group.featured {
featured.dereference(&community_, &context_).await.ok();
}
if let Some(moderators) = group.attributed_to {
moderators.dereference(&community_, &context_).await.ok();
}
Ok(())
});

View file

@ -39,7 +39,6 @@ use lemmy_db_schema::{
site::{Site, SiteInsertForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_utils::{
error::{FederationError, LemmyError, LemmyResult},
@ -163,7 +162,7 @@ impl Object for ApubSite {
banner,
description: apub.summary,
actor_id: Some(apub.id.clone().into()),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
inbox_url: Some(apub.inbox.clone().into()),
public_key: Some(apub.public_key.public_key_pem.clone()),
private_key: None,

View file

@ -35,7 +35,6 @@ use lemmy_db_schema::{
person::{Person as DbPerson, PersonInsertForm, PersonUpdateForm},
},
traits::{ApubActor, Crud},
utils::naive_now,
};
use lemmy_utils::{
error::{LemmyError, LemmyResult},
@ -176,7 +175,7 @@ impl Object for ApubPerson {
bot_account: Some(person.kind == UserTypes::Service),
private_key: None,
public_key: person.public_key.public_key_pem,
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
inbox_url: Some(
person
.endpoints

View file

@ -35,7 +35,6 @@ use lemmy_db_schema::{
post::{Post, PostInsertForm, PostUpdateForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views_actor::structs::CommunityModeratorView;
use lemmy_utils::{
@ -260,7 +259,7 @@ impl Object for ApubPost {
..PostInsertForm::new(name, creator.id, community.id)
};
let timestamp = page.updated.or(page.published).unwrap_or_else(naive_now);
let timestamp = page.updated.or(page.published).unwrap_or_else(Utc::now);
let post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?;
let post_ = post.clone();
let context_ = context.reset_request_count();

View file

@ -31,7 +31,6 @@ use lemmy_db_schema::{
private_message::{PrivateMessage, PrivateMessageInsertForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{
@ -161,7 +160,7 @@ impl Object for ApubPrivateMessage {
ap_id: Some(note.id.into()),
local: Some(false),
};
let timestamp = note.updated.or(note.published).unwrap_or_else(naive_now);
let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now);
let pm = PrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?;
Ok(pm.into())
}

View file

@ -60,9 +60,9 @@ impl<T, S: SelectableExpression<current_value>> SelectableExpression<T> for Valu
impl<T, S: SelectableExpression<current_value>> Insertable<T> for ValuesFromSeries<S>
where
dsl::BareSelect<Self>: AsQuery + Insertable<T>,
dsl::select<Self>: AsQuery + Insertable<T>,
{
type Values = <dsl::BareSelect<Self> as Insertable<T>>::Values;
type Values = <dsl::select<Self> as Insertable<T>>::Values;
fn values(self) -> Self::Values {
dsl::select(self).values()

View file

@ -37,6 +37,8 @@ full = [
"tokio-postgres-rustls",
"rustls",
"i-love-jesus",
"tuplex",
"diesel-bind-if-some",
]
[dependencies]
@ -73,11 +75,12 @@ tokio = { workspace = true, optional = true }
tokio-postgres = { workspace = true, optional = true }
tokio-postgres-rustls = { workspace = true, optional = true }
rustls = { workspace = true, optional = true }
uuid = { workspace = true, features = ["v4"] }
uuid.workspace = true
i-love-jesus = { workspace = true, optional = true }
anyhow = { workspace = true }
moka.workspace = true
diesel-bind-if-some = { workspace = true, optional = true }
derive-new.workspace = true
tuplex = { workspace = true, optional = true }
[dev-dependencies]
serial_test = { workspace = true }

View file

@ -38,7 +38,7 @@ AS $a$
BEGIN
EXECUTE replace($b$
-- When a thing gets a vote, update its aggregates and its creator's aggregates
CALL r.create_triggers ('thing_like', $$
CALL r.create_triggers ('thing_actions', $$
BEGIN
WITH thing_diff AS ( UPDATE
thing_aggregates AS a
@ -46,7 +46,8 @@ BEGIN
score = a.score + diff.upvotes - diff.downvotes, upvotes = a.upvotes + diff.upvotes, downvotes = a.downvotes + diff.downvotes, controversy_rank = r.controversy_rank ((a.upvotes + diff.upvotes)::numeric, (a.downvotes + diff.downvotes)::numeric)
FROM (
SELECT
(thing_like).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (thing_like).thing_id) AS diff
(thing_actions).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_actions).like_score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_actions).like_score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows
WHERE (thing_actions).like_score IS NOT NULL GROUP BY (thing_actions).thing_id) AS diff
WHERE
a.thing_id = diff.thing_id
AND (diff.upvotes, diff.downvotes) != (0, 0)
@ -360,7 +361,7 @@ CREATE TRIGGER comment_count
-- Count subscribers for communities.
-- subscribers should be updated only when a local community is followed by a local or remote person.
-- subscribers_local should be updated only when a local person follows a local or remote community.
CALL r.create_triggers ('community_follower', $$
CALL r.create_triggers ('community_actions', $$
BEGIN
UPDATE
community_aggregates AS a
@ -368,10 +369,11 @@ BEGIN
subscribers = a.subscribers + diff.subscribers, subscribers_local = a.subscribers_local + diff.subscribers_local
FROM (
SELECT
(community_follower).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local
(community_actions).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local
FROM select_old_and_new_rows AS old_and_new_rows
LEFT JOIN community ON community.id = (community_follower).community_id
LEFT JOIN person ON person.id = (community_follower).person_id GROUP BY (community_follower).community_id) AS diff
LEFT JOIN community ON community.id = (community_actions).community_id
LEFT JOIN person ON person.id = (community_actions).person_id
WHERE (community_actions).followed IS NOT NULL GROUP BY (community_actions).community_id) AS diff
WHERE
a.community_id = diff.community_id
AND (diff.subscribers, diff.subscribers_local) != (0, 0);
@ -382,6 +384,44 @@ END;
$$);
CALL r.create_triggers ('post_report', $$
BEGIN
UPDATE
post_aggregates AS a
SET
report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count
FROM (
SELECT
(post_report).post_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (post_report).resolved), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (post_report).post_id) AS diff
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
AND a.post_id = diff.post_id;
RETURN NULL;
END;
$$);
CALL r.create_triggers ('comment_report', $$
BEGIN
UPDATE
comment_aggregates AS a
SET
report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count
FROM (
SELECT
(comment_report).comment_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (comment_report).resolved), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (comment_report).comment_id) AS diff
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
AND a.comment_id = diff.comment_id;
RETURN NULL;
END;
$$);
-- These triggers create and update rows in each aggregates table to match its associated table's rows.
-- Deleting rows and updating IDs are already handled by `CASCADE` in foreign key constraints.
CREATE FUNCTION r.comment_aggregates_from_comment ()
@ -541,7 +581,7 @@ CREATE FUNCTION r.delete_follow_before_person ()
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM community_follower AS c
DELETE FROM community_actions AS c
WHERE c.person_id = OLD.id;
RETURN OLD;
END;

View file

@ -151,3 +151,118 @@ DECLARE
END;
$a$;
-- Edit community aggregates to include voters as active users
CREATE OR REPLACE FUNCTION r.community_aggregates_activity (i text)
RETURNS TABLE (
count_ bigint,
community_id_ integer)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN query
SELECT
count(*),
community_id
FROM (
SELECT
c.creator_id,
p.community_id
FROM
comment c
INNER JOIN post p ON c.post_id = p.id
INNER JOIN person pe ON c.creator_id = pe.id
WHERE
c.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE
UNION
SELECT
p.creator_id,
p.community_id
FROM
post p
INNER JOIN person pe ON p.creator_id = pe.id
WHERE
p.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE
UNION
SELECT
pl.person_id,
p.community_id
FROM
post_like pl
INNER JOIN post p ON pl.post_id = p.id
INNER JOIN person pe ON pl.person_id = pe.id
WHERE
pl.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE
UNION
SELECT
cl.person_id,
p.community_id
FROM
comment_like cl
INNER JOIN comment c ON cl.comment_id = c.id
INNER JOIN post p ON c.post_id = p.id
INNER JOIN person pe ON cl.person_id = pe.id
WHERE
cl.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE) a
GROUP BY
community_id;
END;
$$;
-- Edit site aggregates to include voters and people who have read posts as active users
CREATE OR REPLACE FUNCTION r.site_aggregates_activity (i text)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
count_ integer;
BEGIN
SELECT
count(*) INTO count_
FROM (
SELECT
c.creator_id
FROM
comment c
INNER JOIN person pe ON c.creator_id = pe.id
WHERE
c.published > ('now'::timestamp - i::interval)
AND pe.local = TRUE
AND pe.bot_account = FALSE
UNION
SELECT
p.creator_id
FROM
post p
INNER JOIN person pe ON p.creator_id = pe.id
WHERE
p.published > ('now'::timestamp - i::interval)
AND pe.local = TRUE
AND pe.bot_account = FALSE
UNION
SELECT
pl.person_id
FROM
post_like pl
INNER JOIN person pe ON pl.person_id = pe.id
WHERE
pl.published > ('now'::timestamp - i::interval)
AND pe.local = TRUE
AND pe.bot_account = FALSE
UNION
SELECT
cl.person_id
FROM
comment_like cl
INNER JOIN person pe ON cl.person_id = pe.id
WHERE
cl.published > ('now'::timestamp - i::interval)
AND pe.local = TRUE
AND pe.bot_account = FALSE) a;
RETURN count_;
END;
$$;

View file

@ -65,11 +65,7 @@ mod tests {
);
let inserted_post = Post::create(pool, &new_post).await?;
let post_like = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1);
let _inserted_post_like = PostLike::like(pool, &post_like).await?;
let comment_form = CommentInsertForm::new(

View file

@ -2,10 +2,17 @@ use crate::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
diesel::OptionalExtension,
newtypes::{PersonId, PostId},
schema::person_post_aggregates::dsl::{person_id, person_post_aggregates, post_id},
utils::{get_conn, DbPool},
schema::post_actions,
utils::{find_action, get_conn, now, DbPool},
};
use diesel::{
expression::SelectableHelper,
insert_into,
result::Error,
ExpressionMethods,
NullableExpressionMethods,
QueryDsl,
};
use diesel::{insert_into, result::Error, QueryDsl};
use diesel_async::RunQueryDsl;
impl PersonPostAggregates {
@ -14,11 +21,13 @@ impl PersonPostAggregates {
form: &PersonPostAggregatesForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
insert_into(person_post_aggregates)
let form = (form, post_actions::read_comments.eq(now().nullable()));
insert_into(post_actions::table)
.values(form)
.on_conflict((person_id, post_id))
.on_conflict((post_actions::person_id, post_actions::post_id))
.do_update()
.set(form)
.returning(Self::as_select())
.get_result::<Self>(conn)
.await
}
@ -28,8 +37,8 @@ impl PersonPostAggregates {
post_id_: PostId,
) -> Result<Option<Self>, Error> {
let conn = &mut get_conn(pool).await?;
person_post_aggregates
.find((person_id_, post_id_))
find_action(post_actions::read_comments, (person_id_, post_id_))
.select(Self::as_select())
.first(conn)
.await
.optional()

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