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: when:
- event: pull_request - 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) # make sure api builds with default features (used by other crates relying on lemmy api)
check_api_common_default_features: check_api_common_default_features:
image: *rust_image image: *rust_image
@ -138,26 +168,6 @@ steps:
- diff tmp.schema crates/db_schema/src/schema.rs - diff tmp.schema crates/db_schema/src/schema.rs
when: *slow_check_paths 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: cargo_build:
image: *rust_image image: *rust_image
environment: environment:
@ -167,27 +177,6 @@ steps:
- mv target/debug/lemmy_server target/lemmy_server - mv target/debug/lemmy_server target/lemmy_server
when: *slow_check_paths 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: check_diesel_migration:
# TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server # TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server
image: *rust_image image: *rust_image
@ -221,6 +210,17 @@ steps:
- diff before.sqldump after.sqldump - diff before.sqldump after.sqldump
when: *slow_check_paths 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: run_federation_tests:
image: node:22-bookworm-slim image: node:22-bookworm-slim
environment: environment:
@ -280,24 +280,26 @@ steps:
# using https://github.com/pksunkara/cargo-workspaces # using https://github.com/pksunkara/cargo-workspaces
publish_to_crates_io: publish_to_crates_io:
image: *rust_image image: *rust_image
environment:
CARGO_API_TOKEN:
from_secret: cargo_api_token
commands: commands:
- *install_binstall - *install_binstall
# Install cargo-workspaces # Install cargo-workspaces
- cargo binstall -y cargo-workspaces - cargo binstall -y cargo-workspaces
- cp -r migrations crates/db_schema/ - 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}" - 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: when:
- event: tag - event: tag
notify_on_failure: notify_on_build:
image: alpine:3 image: alpine:3
commands: commands:
- apk add curl - 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: when:
- event: [pull_request, tag] - event: [pull_request, tag]
status: failure status: [failure, success]
notify_on_tag_deploy: notify_on_tag_deploy:
image: alpine:3 image: alpine:3

341
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

@ -41,6 +41,9 @@ afterAll(async () => {
}); });
test("Upload image and delete it", 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 // Before running this test, you need to delete all previous images in the DB
await deleteAllImages(alpha); await deleteAllImages(alpha);

View file

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

View file

@ -1,6 +1,6 @@
jest.setTimeout(120000); jest.setTimeout(120000);
import { FollowCommunity } from "lemmy-js-client"; import { FollowCommunity, LemmyHttp } from "lemmy-js-client";
import { import {
alpha, alpha,
setupLogins, setupLogins,
@ -21,6 +21,9 @@ import {
resolveComment, resolveComment,
likeComment, likeComment,
waitUntil, waitUntil,
gamma,
getPosts,
getComments,
} from "./shared"; } from "./shared";
beforeAll(setupLogins); beforeAll(setupLogins);
@ -47,6 +50,7 @@ test("Follow a private community", async () => {
await resolveCommunity(user, community.community_view.community.actor_id) await resolveCommunity(user, community.community_view.community.actor_id)
).community; ).community;
expect(betaCommunity).toBeDefined(); expect(betaCommunity).toBeDefined();
expect(betaCommunity?.community.visibility).toBe("Private");
const betaCommunityId = betaCommunity!.community.id; const betaCommunityId = betaCommunity!.community.id;
const follow_form: FollowCommunity = { const follow_form: FollowCommunity = {
community_id: betaCommunityId, community_id: betaCommunityId,
@ -148,16 +152,7 @@ test("Only followers can view and interact with private community content", asyn
follow: true, follow: true,
}; };
await user.followCommunity(follow_form); await user.followCommunity(follow_form);
const pendingFollows1 = await waitUntil( approveFollower(alpha, alphaCommunityId);
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
const approve = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows1.items[0].person.id,
);
expect(approve.success).toBe(true);
// now user can fetch posts and comments in community (using signed fetch), and create posts // now user can fetch posts and comments in community (using signed fetch), and create posts
await waitUntil( await waitUntil(
@ -212,3 +207,151 @@ test("Reject follower", async () => {
c => c.community_view.subscribed == "NotSubscribed", 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 { import {
AdminBlockInstanceParams,
ApproveCommunityPendingFollower, ApproveCommunityPendingFollower,
BlockCommunity, BlockCommunity,
BlockCommunityResponse, BlockCommunityResponse,
BlockInstance,
BlockInstanceResponse,
CommunityId, CommunityId,
CommunityVisibility, CommunityVisibility,
CreatePrivateMessageReport, CreatePrivateMessageReport,
DeleteImage, DeleteImage,
EditCommunity, EditCommunity,
GetCommunityPendingFollowsCount,
GetCommunityPendingFollowsCountResponse, GetCommunityPendingFollowsCountResponse,
GetReplies, GetReplies,
GetRepliesResponse, GetRepliesResponse,
@ -22,11 +20,13 @@ import {
PostView, PostView,
PrivateMessageReportResponse, PrivateMessageReportResponse,
SuccessResponse, SuccessResponse,
UserBlockInstanceParams,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
import { DeletePost } from "lemmy-js-client/dist/types/DeletePost"; import { DeletePost } from "lemmy-js-client/dist/types/DeletePost";
import { EditPost } from "lemmy-js-client/dist/types/EditPost"; import { EditPost } from "lemmy-js-client/dist/types/EditPost";
import { EditSite } from "lemmy-js-client/dist/types/EditSite"; 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 { FeaturePost } from "lemmy-js-client/dist/types/FeaturePost";
import { GetComments } from "lemmy-js-client/dist/types/GetComments"; import { GetComments } from "lemmy-js-client/dist/types/GetComments";
import { GetCommentsResponse } from "lemmy-js-client/dist/types/GetCommentsResponse"; 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 delta = new LemmyHttp(deltaUrl, { fetchFunction });
export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
export const betaAllowedInstances = [
"lemmy-alpha",
"lemmy-gamma",
"lemmy-delta",
"lemmy-epsilon",
];
const password = "lemmylemmy"; const password = "lemmylemmy";
export async function setupLogins() { export async function setupLogins() {
@ -169,30 +162,29 @@ export async function setupLogins() {
rate_limit_comment: 999, rate_limit_comment: 999,
rate_limit_search: 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); await alpha.editSite(editSiteForm);
editSiteForm.allowed_instances = betaAllowedInstances;
await beta.editSite(editSiteForm); await beta.editSite(editSiteForm);
editSiteForm.allowed_instances = [
"lemmy-alpha",
"lemmy-beta",
"lemmy-delta",
"lemmy-epsilon",
];
await gamma.editSite(editSiteForm); await gamma.editSite(editSiteForm);
// Setup delta allowed instance
editSiteForm.allowed_instances = ["lemmy-beta"];
await delta.editSite(editSiteForm); 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 // Create the main alpha/beta communities
// Ignore thrown errors of duplicates // 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( export async function createPost(
api: LemmyHttp, api: LemmyHttp,
community_id: number, community_id: number,
@ -855,16 +858,16 @@ export function getPosts(
return api.getPosts(form); return api.getPosts(form);
} }
export function blockInstance( export function userBlockInstance(
api: LemmyHttp, api: LemmyHttp,
instance_id: InstanceId, instance_id: InstanceId,
block: boolean, block: boolean,
): Promise<BlockInstanceResponse> { ): Promise<SuccessResponse> {
let form: BlockInstance = { let form: UserBlockInstanceParams = {
instance_id, instance_id,
block, block,
}; };
return api.blockInstance(form); return api.userBlockInstance(form);
} }
export function blockCommunity( export function blockCommunity(
@ -988,7 +991,7 @@ export function getCommentParentId(comment: Comment): number | undefined {
if (split.length > 1) { if (split.length > 1) {
return Number(split[split.length - 2]); return Number(split[split.length - 2]);
} else { } 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; return undefined;
} }
} }
@ -1006,7 +1009,7 @@ export async function waitUntil<T>(
result = await fetcher(); result = await fetcher();
if (checker(result)) return result; if (checker(result)) return result;
} catch (error) { } catch (error) {
//console.error(error); console.error(error);
} }
await delay( await delay(
delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000, delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000,

View file

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

View file

@ -73,6 +73,15 @@
# #
# Requires pict-rs 0.5 # Requires pict-rs 0.5
"ProxyAllImages" "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) # Timeout for uploading images to pictrs (in seconds)
upload_timeout: 30 upload_timeout: 30
# Resize post thumbnails to this maximum width/height. # Resize post thumbnails to this maximum width/height.
@ -122,5 +131,5 @@
} }
# Sets a response Access-Control-Allow-Origin CORS header # Sets a response Access-Control-Allow-Origin CORS header
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin # 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 } chrono = { workspace = true }
url = { workspace = true } url = { workspace = true }
hound = "3.5.1" hound = "3.5.1"
sitemap-rs = "0.2.1" sitemap-rs = "0.2.2"
totp-rs = { version = "5.6.0", features = ["gen_secret", "otpauth"] } totp-rs = { version = "5.6.0", features = ["gen_secret", "otpauth"] }
actix-web-httpauth = "0.8.2" actix-web-httpauth = "0.8.2"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ use lemmy_db_schema::source::{
login_token::LoginToken, login_token::LoginToken,
password_reset_request::PasswordResetRequest, password_reset_request::PasswordResetRequest,
}; };
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn change_password_after_reset( 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 // Update the user with the new password
let password = data.password.clone(); let password = data.password.clone();
LocalUser::update_password(&mut context.pool(), local_user_id, &password) LocalUser::update_password(&mut context.pool(), local_user_id, &password).await?;
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
LoginToken::invalidate_all(&mut context.pool(), local_user_id).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::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{CaptchaResponse, GetCaptchaResponse}, person::{CaptchaResponse, GetCaptchaResponse},
LemmyErrorType,
}; };
use lemmy_db_schema::source::{ use lemmy_db_schema::source::{
captcha_answer::{CaptchaAnswer, CaptchaAnswerForm}, 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 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)?; let wav = captcha_as_wav_base64(&captcha)?;

View file

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

View file

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

View file

@ -1,34 +1,39 @@
use actix_web::web::{Data, Json}; 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_schema::source::post::PostHide;
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use std::collections::HashSet;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn hide_post( pub async fn hide_post(
data: Json<HidePost>, data: Json<HidePost>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_ids = HashSet::from_iter(data.post_ids.clone());
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_id = data.post_id;
// Mark the post as hidden / unhidden // Mark the post as hidden / unhidden
if data.hide { if data.hide {
PostHide::hide(&mut context.pool(), post_ids, person_id) PostHide::hide(&mut context.pool(), post_id, person_id)
.await .await
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?; .with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
} else { } else {
PostHide::unhide(&mut context.pool(), post_ids, person_id) PostHide::unhide(&mut context.pool(), post_id, person_id)
.await .await
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?; .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, context::LemmyContext,
post::{CreatePostLike, PostResponse}, post::{CreatePostLike, PostResponse},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{ utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
check_bot_account,
check_community_user_action,
check_local_vote_mode,
mark_post_as_read,
VoteItem,
},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
local_site::LocalSite, local_site::LocalSite,
post::{PostLike, PostLikeForm}, post::{PostLike, PostLikeForm, PostRead, PostReadForm},
}, },
traits::Likeable, traits::Likeable,
}; };
@ -53,11 +47,7 @@ pub async fn like_post(
) )
.await?; .await?;
let like_form = PostLikeForm { let like_form = PostLikeForm::new(data.post_id, local_user_view.person.id, data.score);
post_id: data.post_id,
person_id: local_user_view.person.id,
score: data.score,
};
// Remove any likes first // Remove any likes first
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
@ -72,7 +62,9 @@ pub async fn like_post(
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?; .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( ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment { SendActivityData::LikePostOrComment {

View file

@ -9,7 +9,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
moderator::{ModLockPost, ModLockPostForm}, mod_log::moderator::{ModLockPost, ModLockPostForm},
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },
traits::Crud, 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 actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse}; use lemmy_api_common::{
use lemmy_db_schema::source::post::PostRead; context::LemmyContext,
use lemmy_db_views::structs::LocalUserView; post::{MarkPostAsRead, PostResponse},
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS}; };
use std::collections::HashSet; use lemmy_db_schema::source::post::{PostRead, PostReadForm};
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn mark_post_as_read( pub async fn mark_post_as_read(
data: Json<MarkPostAsRead>, data: Json<MarkPostAsRead>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_ids = HashSet::from_iter(data.post_ids.clone());
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_id = data.post_id;
// Mark the post as read / unread // Mark the post as read / unread
let form = PostReadForm::new(post_id, person_id);
if data.read { if data.read {
PostRead::mark_as_read(&mut context.pool(), post_ids, person_id) PostRead::mark_as_read(&mut context.pool(), &form).await?;
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
} else { } else {
PostRead::mark_as_unread(&mut context.pool(), post_ids, person_id) PostRead::mark_as_unread(&mut context.pool(), &form).await?;
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
} }
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 like;
pub mod list_post_likes; pub mod list_post_likes;
pub mod lock; pub mod lock;
pub mod mark_many_read;
pub mod mark_read; pub mod mark_read;
pub mod save; pub mod save;

View file

@ -2,10 +2,9 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
post::{PostResponse, SavePost}, post::{PostResponse, SavePost},
utils::mark_post_as_read,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::post::{PostSaved, PostSavedForm}, source::post::{PostRead, PostReadForm, PostSaved, PostSavedForm},
traits::Saveable, traits::Saveable,
}; };
use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_db_views::structs::{LocalUserView, PostView};
@ -17,10 +16,7 @@ pub async fn save_post(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_saved_form = PostSavedForm { let post_saved_form = PostSavedForm::new(data.post_id, local_user_view.person.id);
post_id: data.post_id,
person_id: local_user_view.person.id,
};
if data.save { if data.save {
PostSaved::save(&mut context.pool(), &post_saved_form) PostSaved::save(&mut context.pool(), &post_saved_form)
@ -42,7 +38,8 @@ pub async fn save_post(
) )
.await?; .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 })) 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, language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm}, mod_log::moderator::{ModAdd, ModAddForm},
oauth_provider::OAuthProvider, oauth_provider::OAuthProvider,
tagline::Tagline, 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 federated_instances;
pub mod leave_admin; pub mod leave_admin;
pub mod list_all_media; pub mod list_all_media;
pub mod mod_log; pub mod mod_log;
pub mod purge; pub mod purge;
pub mod registration_applications; 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_schema::{source::local_site::LocalSite, ModlogActionType};
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_moderator::structs::{ use lemmy_db_views_moderator::structs::{
AdminAllowInstanceView,
AdminBlockInstanceView,
AdminPurgeCommentView, AdminPurgeCommentView,
AdminPurgeCommunityView, AdminPurgeCommunityView,
AdminPurgePersonView, AdminPurgePersonView,
@ -121,6 +123,8 @@ pub async fn get_mod_log(
admin_purged_communities, admin_purged_communities,
admin_purged_posts, admin_purged_posts,
admin_purged_comments, admin_purged_comments,
admin_block_instance,
admin_allow_instance,
) = if data.community_id.is_none() { ) = if data.community_id.is_none() {
( (
match type_ { match type_ {
@ -161,6 +165,18 @@ pub async fn get_mod_log(
} }
_ => Default::default(), _ => 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 { } else {
Default::default() Default::default()
@ -183,5 +199,7 @@ pub async fn get_mod_log(
admin_purged_posts, admin_purged_posts,
admin_purged_comments, admin_purged_comments,
hidden_communities, hidden_communities,
admin_block_instance,
admin_allow_instance,
})) }))
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,6 +36,7 @@ full = [
"futures", "futures",
"jsonwebtoken", "jsonwebtoken",
"mime", "mime",
"moka",
] ]
[dependencies] [dependencies]
@ -58,22 +59,18 @@ uuid = { workspace = true, optional = true }
tokio = { workspace = true, optional = true } tokio = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true } reqwest = { workspace = true, optional = true }
ts-rs = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true }
moka.workspace = true moka = { workspace = true, optional = true }
anyhow.workspace = true anyhow.workspace = true
actix-web = { workspace = true, optional = true } actix-web = { workspace = true, optional = true }
enum-map = { workspace = true } enum-map = { workspace = true }
urlencoding = { workspace = true } urlencoding = { workspace = true }
mime = { version = "0.3.17", optional = true } mime = { version = "0.3.17", optional = true }
mime_guess = "2.0.5"
webpage = { version = "2.0", default-features = false, features = [ webpage = { version = "2.0", default-features = false, features = [
"serde", "serde",
], optional = true } ], 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 } 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] [dev-dependencies]
serial_test = { workspace = true } serial_test = { workspace = true }

View file

@ -17,8 +17,10 @@ use lemmy_db_schema::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
comment::Comment, comment::Comment,
comment_reply::{CommentReply, CommentReplyInsertForm}, comment_reply::{CommentReply, CommentReplyInsertForm},
community::Community,
person::Person, person::Person,
person_mention::{PersonMention, PersonMentionInsertForm}, person_mention::{PersonMention, PersonMentionInsertForm},
post::Post,
}, },
traits::Crud, traits::Crud,
}; };
@ -101,17 +103,28 @@ pub async fn send_local_notifs(
let mut recipient_ids = Vec::new(); let mut recipient_ids = Vec::new();
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
// let person = my_local_user.person; // When called from api code, we have local user view and can read with CommentView
// Read the comment view to get extra info // 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( let comment_view = CommentView::read(
&mut context.pool(), &mut context.pool(),
comment_id, comment_id,
local_user_view.map(|view| &view.local_user), Some(&local_user_view.local_user),
) )
.await?; .await?;
let comment = comment_view.comment; (
let post = comment_view.post; comment_view.comment,
let community = comment_view.community; 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 // Send the local mentions
for mention in 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. /// Initialize a context for use in tests which blocks federation network calls.
/// ///
/// Do not use this in production code. /// Do not use this in production code.
#[allow(clippy::expect_used)]
pub async fn init_test_federation_config() -> FederationConfig<LemmyContext> { pub async fn init_test_federation_config() -> FederationConfig<LemmyContext> {
// call this to run migrations // call this to run migrations
let pool = build_db_pool_for_tests(); let pool = build_db_pool_for_tests();

View file

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

View file

@ -18,12 +18,11 @@ use lemmy_db_schema::{
}, },
}; };
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
settings::structs::{PictrsImageMode, Settings}, settings::structs::{PictrsImageMode, Settings},
REQWEST_TIMEOUT, REQWEST_TIMEOUT,
VERSION, VERSION,
}; };
use mime::Mime;
use reqwest::{ use reqwest::{
header::{CONTENT_TYPE, RANGE}, header::{CONTENT_TYPE, RANGE},
Client, 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 // server may ignore this and still respond with the full response
.header(RANGE, format!("bytes=0-{}", bytes_to_fetch - 1)) /* -1 because inclusive */ .header(RANGE, format!("bytes=0-{}", bytes_to_fetch - 1)) /* -1 because inclusive */
.send() .send()
.await?; .await?
.error_for_status()?;
let content_type: Option<Mime> = response // 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() .headers()
.get(CONTENT_TYPE) .get(CONTENT_TYPE)
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.and_then(|h| h.parse().ok()); .and_then(|h| h.parse().ok()),
);
let opengraph_data = { let opengraph_data = {
// if the content type is not text/html, we don't need to parse it // 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) .timeout(REQWEST_TIMEOUT)
.header("x-api-token", pictrs_api_key) .header("x-api-token", pictrs_api_key)
.send() .send()
.await?; .await?
.error_for_status()?;
let response: PictrsPurgeResponse = response.json().await.map_err(LemmyError::from)?; let response: PictrsPurgeResponse = response.json().await.map_err(LemmyError::from)?;
@ -333,8 +343,8 @@ pub async fn delete_image_from_pictrs(
.delete(&url) .delete(&url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()
.await .await?
.map_err(LemmyError::from)?; .error_for_status()?;
Ok(()) Ok(())
} }
@ -366,6 +376,7 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()
.await? .await?
.error_for_status()?
.json::<PictrsResponse>() .json::<PictrsResponse>()
.await?; .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 // 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 proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}");
let res = context context
.client() .client()
.get(&proxy_url) .get(&proxy_url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()
.await? .await?
.status(); .error_for_status()
if !res.is_success() { .with_lemmy_type(LemmyErrorType::NotAnImageType)?;
Err(LemmyErrorType::NotAnImageType)?
}
let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}"); 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) .timeout(REQWEST_TIMEOUT)
.send() .send()
.await? .await?
.error_for_status()?
.json() .json()
.await?; .await?;
@ -521,7 +531,7 @@ mod tests {
// root relative url // root relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='/image.jpg'></head><body></body></html>"; 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!( assert_eq!(
metadata.image, metadata.image,
Some(Url::parse("https://example.com/image.jpg")?.into()) Some(Url::parse("https://example.com/image.jpg")?.into())
@ -529,7 +539,7 @@ mod tests {
// base relative url // base relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='image.jpg'></head><body></body></html>"; 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!( assert_eq!(
metadata.image, metadata.image,
Some(Url::parse("https://example.com/one/image.jpg")?.into()) Some(Url::parse("https://example.com/one/image.jpg")?.into())
@ -537,7 +547,7 @@ mod tests {
// absolute url // 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 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!( assert_eq!(
metadata.image, metadata.image,
Some(Url::parse("https://cdn.host.com/image.jpg")?.into()) Some(Url::parse("https://cdn.host.com/image.jpg")?.into())
@ -545,7 +555,7 @@ mod tests {
// protocol relative url // 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 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!( assert_eq!(
metadata.image, metadata.image,
Some(Url::parse("https://example.com/image.jpg")?.into()) Some(Url::parse("https://example.com/image.jpg")?.into())

View file

@ -43,6 +43,8 @@ use lemmy_db_views_actor::structs::{
PersonView, PersonView,
}; };
use lemmy_db_views_moderator::structs::{ use lemmy_db_views_moderator::structs::{
AdminAllowInstanceView,
AdminBlockInstanceView,
AdminPurgeCommentView, AdminPurgeCommentView,
AdminPurgeCommunityView, AdminPurgeCommunityView,
AdminPurgePersonView, AdminPurgePersonView,
@ -183,6 +185,8 @@ pub struct GetModlogResponse {
pub admin_purged_posts: Vec<AdminPurgePostView>, pub admin_purged_posts: Vec<AdminPurgePostView>,
pub admin_purged_comments: Vec<AdminPurgeCommentView>, pub admin_purged_comments: Vec<AdminPurgeCommentView>,
pub hidden_communities: Vec<ModHideCommunityView>, pub hidden_communities: Vec<ModHideCommunityView>,
pub admin_block_instance: Vec<AdminBlockInstanceView>,
pub admin_allow_instance: Vec<AdminAllowInstanceView>,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -265,10 +269,6 @@ pub struct CreateSite {
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub captcha_difficulty: Option<String>, pub captcha_difficulty: Option<String>,
#[cfg_attr(feature = "full", ts(optional))] #[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>, pub registration_mode: Option<RegistrationMode>,
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub oauth_registration: Option<bool>, pub oauth_registration: Option<bool>,
@ -394,12 +394,6 @@ pub struct EditSite {
/// The captcha difficulty. Can be easy, medium, or hard /// The captcha difficulty. Can be easy, medium, or hard
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub captcha_difficulty: Option<String>, 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 /// A list of blocked URLs
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub blocked_urls: Option<Vec<String>>, pub blocked_urls: Option<Vec<String>>,
@ -514,6 +508,7 @@ pub struct ReadableFederationState {
next_retry: Option<DateTime<Utc>>, next_retry: Option<DateTime<Utc>>,
} }
#[allow(clippy::expect_used)]
impl From<FederationQueueState> for ReadableFederationState { impl From<FederationQueueState> for ReadableFederationState {
fn from(internal_state: FederationQueueState) -> Self { fn from(internal_state: FederationQueueState) -> Self {
ReadableFederationState { ReadableFederationState {
@ -647,15 +642,29 @@ pub struct GetUnreadRegistrationApplicationCountResponse {
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Block an instance as user /// Block an instance as user
pub struct BlockInstance { pub struct UserBlockInstanceParams {
pub instance_id: InstanceId, pub instance_id: InstanceId,
pub block: bool, pub block: bool,
} }
#[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
pub struct BlockInstanceResponse { pub struct AdminBlockInstanceParams {
pub blocked: bool, 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::LocalSite,
local_site_rate_limit::LocalSiteRateLimit, local_site_rate_limit::LocalSiteRateLimit,
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm}, mod_log::moderator::{
ModRemoveComment,
ModRemoveCommentForm,
ModRemovePost,
ModRemovePostForm,
},
oauth_account::OAuthAccount, oauth_account::OAuthAccount,
password_reset_request::PasswordResetRequest, password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
person_block::PersonBlock, person_block::PersonBlock,
post::{Post, PostLike, PostRead}, post::{Post, PostLike},
registration_application::RegistrationApplication, registration_application::RegistrationApplication,
site::Site, site::Site,
}, },
@ -60,12 +65,13 @@ use lemmy_utils::{
slurs::{build_slur_regex, remove_slurs}, slurs::{build_slur_regex, remove_slurs},
validation::clean_urls_in_text, validation::clean_urls_in_text,
}, },
CacheLock,
CACHE_DURATION_FEDERATION, CACHE_DURATION_FEDERATION,
}; };
use moka::future::Cache; use moka::future::Cache;
use regex::{escape, Regex, RegexSet}; use regex::{escape, Regex, RegexSet};
use rosetta_i18n::{Language, LanguageId}; use rosetta_i18n::{Language, LanguageId};
use std::{collections::HashSet, sync::LazyLock}; use std::sync::LazyLock;
use tracing::warn; use tracing::warn;
use url::{ParseError, Url}; use url::{ParseError, Url};
use urlencoding::encode; 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. /// Updates the read comment count for a post. Usually done when reading or creating a new comment.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn update_read_comments( pub async fn update_read_comments(
@ -166,7 +159,6 @@ pub async fn update_read_comments(
person_id, person_id,
post_id, post_id,
read_comments, read_comments,
..PersonPostAggregatesForm::default()
}; };
PersonPostAggregates::upsert(pool, &person_post_agg_form).await?; PersonPostAggregates::upsert(pool, &person_post_agg_form).await?;
@ -456,7 +448,11 @@ pub async fn send_password_reset_email(
// Generate a random token // Generate a random token
let token = uuid::Uuid::new_v4().to_string(); 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 lang = get_interface_language(user);
let subject = &lang.password_reset_subject(&user.person.name); let subject = &lang.password_reset_subject(&user.person.name);
let protocol_and_hostname = settings.get_protocol_and_hostname(); 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) lang_str_to_lang(&user.local_user.interface_language)
} }
#[allow(clippy::expect_used)]
fn lang_str_to_lang(lang: &str) -> Lang { fn lang_str_to_lang(lang: &str) -> Lang {
let lang_id = LanguageId::new(lang); let lang_id = LanguageId::new(lang);
Lang::from_language_id(&lang_id).unwrap_or_else(|| { 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()) 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 local_site
.as_ref() .as_ref()
.map(local_site_to_slur_regex) .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> { 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() Cache::builder()
.max_capacity(1) .max_capacity(1)
.time_to_live(CACHE_DURATION_FEDERATION) .time_to_live(CACHE_DURATION_FEDERATION)
@ -571,7 +568,11 @@ pub async fn send_application_approved_email(
user: &LocalUserView, user: &LocalUserView,
settings: &Settings, settings: &Settings,
) -> LemmyResult<()> { ) -> 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 lang = get_interface_language(user);
let subject = lang.registration_approved_subject(&user.person.actor_id); let subject = lang.registration_approved_subject(&user.person.actor_id);
let body = lang.registration_approved_body(&settings.hostname); 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 { 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 lang = get_interface_language_from_settings(admin);
let subject = lang.new_application_subject(&settings.hostname, applicant_username); let subject = lang.new_application_subject(&settings.hostname, applicant_username);
let body = lang.new_application_body(applications_link); let body = lang.new_application_body(applications_link);
@ -615,12 +620,14 @@ pub async fn send_new_report_email_to_admins(
let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),); let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),);
for admin in &admins { for admin in &admins {
let email = &admin.local_user.email.clone().expect("email"); if let Some(email) = &admin.local_user.email {
let lang = get_interface_language_from_settings(admin); let lang = get_interface_language_from_settings(admin);
let subject = lang.new_report_subject(&settings.hostname, reported_username, reporter_username); let subject =
lang.new_report_subject(&settings.hostname, reported_username, reporter_username);
let body = lang.new_report_body(reports_link); let body = lang.new_report_body(reports_link);
send_email(&subject, email, &admin.person.name, &body, settings).await?; send_email(&subject, email, &admin.person.name, &body, settings).await?;
} }
}
Ok(()) Ok(())
} }
@ -1044,7 +1051,7 @@ pub fn check_conflicting_like_filters(
pub async fn process_markdown( pub async fn process_markdown(
text: &str, text: &str,
slur_regex: &Option<Regex>, slur_regex: &Option<LemmyResult<Regex>>,
url_blocklist: &RegexSet, url_blocklist: &RegexSet,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<String> { ) -> LemmyResult<String> {
@ -1076,7 +1083,7 @@ pub async fn process_markdown(
pub async fn process_markdown_opt( pub async fn process_markdown_opt(
text: &Option<String>, text: &Option<String>,
slur_regex: &Option<Regex>, slur_regex: &Option<LemmyResult<Regex>>,
url_blocklist: &RegexSet, url_blocklist: &RegexSet,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<Option<String>> { ) -> LemmyResult<Option<String>> {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,17 +12,15 @@ use lemmy_api_common::{
get_url_blocklist, get_url_blocklist,
honeypot_check, honeypot_check,
local_site_to_slur_regex, local_site_to_slur_regex,
mark_post_as_read,
process_markdown_opt, process_markdown_opt,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
impls::actor_language::default_post_language, impls::actor_language::validate_post_language,
source::{ source::{
actor_language::CommunityLanguage,
community::Community, community::Community,
local_site::LocalSite, local_site::LocalSite,
post::{Post, PostInsertForm, PostLike, PostLikeForm}, post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostReadForm},
}, },
traits::{Crud, Likeable}, traits::{Crud, Likeable},
utils::diesel_url_create, utils::diesel_url_create,
@ -98,22 +96,12 @@ pub async fn create_post(
.await?; .await?;
} }
// attempt to set default language if none was provided let language_id = validate_post_language(
let language_id = match data.language_id {
Some(lid) => lid,
None => {
default_post_language(
&mut context.pool(), &mut context.pool(),
community.id, data.language_id,
data.community_id,
local_user_view.local_user.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?; .await?;
let scheduled_publish_time = let scheduled_publish_time =
@ -154,17 +142,14 @@ pub async fn create_post(
// They like their own post by default // They like their own post by default
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_id = inserted_post.id; let post_id = inserted_post.id;
let like_form = PostLikeForm { let like_form = PostLikeForm::new(post_id, person_id, 1);
post_id,
person_id,
score: 1,
};
PostLike::like(&mut context.pool(), &like_form) PostLike::like(&mut context.pool(), &like_form)
.await .await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?; .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 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::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
post::{GetPost, GetPostResponse}, 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::{ use lemmy_db_schema::{
source::{comment::Comment, post::Post}, source::{
comment::Comment,
post::{Post, PostRead, PostReadForm},
},
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::{ use lemmy_db_views::{
@ -62,7 +65,8 @@ pub async fn get_post(
let post_id = post_view.post.id; let post_id = post_view.post.id;
if let Some(person_id) = person_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( update_read_comments(
person_id, person_id,

View file

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

View file

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

View file

@ -1,5 +1,6 @@
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
private_message::{EditPrivateMessage, PrivateMessageResponse}, private_message::{EditPrivateMessage, PrivateMessageResponse},
@ -12,7 +13,6 @@ use lemmy_db_schema::{
private_message::{PrivateMessage, PrivateMessageUpdateForm}, private_message::{PrivateMessage, PrivateMessageUpdateForm},
}, },
traits::Crud, traits::Crud,
utils::naive_now,
}; };
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
use lemmy_utils::{ use lemmy_utils::{
@ -47,7 +47,7 @@ pub async fn update_private_message(
private_message_id, private_message_id,
&PrivateMessageUpdateForm { &PrivateMessageUpdateForm {
content: Some(content), content: Some(content),
updated: Some(Some(naive_now())), updated: Some(Some(Utc::now())),
..Default::default() ..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 crate::site::{application_question_check, site_default_post_listing_type_check};
use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair}; use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
use actix_web::web::Json; use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
site::{CreateSite, SiteResponse}, site::{CreateSite, SiteResponse},
@ -23,7 +24,7 @@ use lemmy_db_schema::{
site::{Site, SiteUpdateForm}, site::{Site, SiteUpdateForm},
}, },
traits::Crud, 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_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{ use lemmy_utils::{
@ -75,7 +76,7 @@ pub async fn create_site(
icon: Some(icon), icon: Some(icon),
banner: Some(banner), banner: Some(banner),
actor_id: Some(actor_id), actor_id: Some(actor_id),
last_refreshed_at: Some(naive_now()), last_refreshed_at: Some(Utc::now()),
inbox_url, inbox_url,
private_key: Some(Some(keypair.private_key)), private_key: Some(Some(keypair.private_key)),
public_key: Some(keypair.public_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()), legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins, application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names, 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()), slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
actor_name_max_length: data.actor_name_max_length, actor_name_max_length: data.actor_name_max_length,
federation_enabled: data.federation_enabled, federation_enabled: data.federation_enabled,
@ -161,7 +162,7 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
.slur_filter_regex .slur_filter_regex
.as_deref() .as_deref()
.or(local_site.slur_filter_regex.as_deref()), .or(local_site.slur_filter_regex.as_deref()),
)?; );
site_name_length_check(&create_site.name)?; site_name_length_check(&create_site.name)?;
check_slurs(&create_site.name, &slur_regex)?; 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::structs::{LocalUserView, SiteView};
use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView}; use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, build_cache,
CACHE_DURATION_API, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
CacheLock,
VERSION, VERSION,
}; };
use moka::future::Cache;
use std::sync::LazyLock; use std::sync::LazyLock;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -28,41 +28,10 @@ pub async fn get_site(
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GetSiteResponse>> { ) -> 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 // 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 let mut site_response = CACHE
.try_get_with::<_, LemmyError>((), async { .try_get_with((), read_site(&context))
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![],
})
})
.await .await
.map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?; .map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?;
@ -112,3 +81,29 @@ pub async fn get_site(
Ok(Json(site_response)) 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 crate::site::{application_question_check, site_default_post_listing_type_check};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
request::replace_image, request::replace_image,
@ -18,8 +19,6 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
actor_language::SiteLanguage, actor_language::SiteLanguage,
federation_allowlist::FederationAllowList,
federation_blocklist::FederationBlockList,
local_site::{LocalSite, LocalSiteUpdateForm}, local_site::{LocalSite, LocalSiteUpdateForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
@ -27,7 +26,7 @@ use lemmy_db_schema::{
site::{Site, SiteUpdateForm}, site::{Site, SiteUpdateForm},
}, },
traits::Crud, traits::Crud,
utils::{diesel_string_update, diesel_url_update, naive_now}, utils::{diesel_string_update, diesel_url_update},
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
@ -88,7 +87,7 @@ pub async fn update_site(
icon, icon,
banner, banner,
content_warning: diesel_string_update(data.content_warning.as_deref()), content_warning: diesel_string_update(data.content_warning.as_deref()),
updated: Some(Some(naive_now())), updated: Some(Some(Utc::now())),
..Default::default() ..Default::default()
}; };
@ -111,7 +110,7 @@ pub async fn update_site(
legal_information: diesel_string_update(data.legal_information.as_deref()), legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins, application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names, 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()), slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
actor_name_max_length: data.actor_name_max_length, actor_name_max_length: data.actor_name_max_length,
federation_enabled: data.federation_enabled, federation_enabled: data.federation_enabled,
@ -151,12 +150,6 @@ pub async fn update_site(
.await .await
.ok(); .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() { if let Some(url_blocklist) = data.blocked_urls.clone() {
let parsed_urls = check_urls_are_valid(&url_blocklist)?; let parsed_urls = check_urls_are_valid(&url_blocklist)?;
LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?; 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 .slur_filter_regex
.as_deref() .as_deref()
.or(local_site.slur_filter_regex.as_deref()), .or(local_site.slur_filter_regex.as_deref()),
)?; );
if let Some(name) = &edit_site.name { if let Some(name) = &edit_site.name {
// The name doesn't need to be updated, but if provided it cannot be blanked out... // 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 activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
tagline::{TaglineResponse, UpdateTagline}, tagline::{TaglineResponse, UpdateTagline},
@ -11,7 +12,6 @@ use lemmy_db_schema::{
tagline::{Tagline, TaglineUpdateForm}, tagline::{Tagline, TaglineUpdateForm},
}, },
traits::Crud, traits::Crud,
utils::naive_now,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
@ -33,7 +33,7 @@ pub async fn update_tagline(
let tagline_form = TaglineUpdateForm { let tagline_form = TaglineUpdateForm {
content, content,
updated: naive_now(), updated: Utc::now(),
}; };
let tagline = Tagline::update(&mut context.pool(), data.id, &tagline_form).await?; 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::{ use lemmy_db_schema::{
aggregates::structs::PersonAggregates, aggregates::structs::PersonAggregates,
newtypes::{InstanceId, OAuthProviderId}, newtypes::{InstanceId, OAuthProviderId, SiteId},
source::{ source::{
actor_language::SiteLanguage,
captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer},
language::Language, language::Language,
local_site::LocalSite, local_site::LocalSite,
@ -145,18 +146,25 @@ pub async fn register(
..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string())) ..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 { if local_site.site_setup && require_registration_application {
if let Some(answer) = data.answer.clone() {
// Create the registration application // Create the registration application
let form = RegistrationApplicationInsertForm { let form = RegistrationApplicationInsertForm {
local_user_id: inserted_local_user.id, local_user_id: inserted_local_user.id,
// We already made sure answer was not null above answer,
answer: data.answer.clone().expect("must have an 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 // Email the admins, only if email verification is not required
if local_site.application_email_admins && !local_site.require_email_verification { if local_site.application_email_admins && !local_site.require_email_verification {
@ -304,7 +312,7 @@ pub async fn authenticate_with_oauth(
OAuthAccount::create(&mut context.pool(), &oauth_account_form) OAuthAccount::create(&mut context.pool(), &oauth_account_form)
.await .await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?; .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
local_user = user_view.local_user.clone(); local_user = user_view.local_user.clone();
} else { } else {
@ -357,7 +365,13 @@ pub async fn authenticate_with_oauth(
..LocalUserInsertForm::new(person.id, None) ..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 // Create the oauth account
let oauth_account_form = let oauth_account_form =
@ -365,7 +379,7 @@ pub async fn authenticate_with_oauth(
OAuthAccount::create(&mut context.pool(), &oauth_account_form) OAuthAccount::create(&mut context.pool(), &oauth_account_form)
.await .await
.map_err(|_| LemmyErrorType::IncorrectLogin)?; .with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
// prevent sign in until application is accepted // prevent sign in until application is accepted
if local_site.site_setup if local_site.site_setup
@ -373,18 +387,20 @@ pub async fn authenticate_with_oauth(
&& !local_user.accepted_application && !local_user.accepted_application
&& !local_user.admin && !local_user.admin
{ {
if let Some(answer) = data.answer.clone() {
// Create the registration application // Create the registration application
RegistrationApplication::create( RegistrationApplication::create(
&mut context.pool(), &mut context.pool(),
&RegistrationApplicationInsertForm { &RegistrationApplicationInsertForm {
local_user_id: local_user.id, local_user_id: local_user.id,
answer: data.answer.clone().expect("must have an answer"), answer,
}, },
) )
.await?; .await?;
login_response.registration_created = true; login_response.registration_created = true;
} }
}
// Check email is verified when required // Check email is verified when required
login_response.verify_email_sent = login_response.verify_email_sent =
@ -446,15 +462,23 @@ async fn create_local_user(
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
language_tags: Vec<String>, language_tags: Vec<String>,
local_user_form: &LocalUserInsertForm, local_user_form: &LocalUserInsertForm,
local_site_id: SiteId,
) -> Result<LocalUser, LemmyError> { ) -> Result<LocalUser, LemmyError> {
let all_languages = Language::read_all(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?;
// use hashset to avoid duplicates // use hashset to avoid duplicates
let mut language_ids = HashSet::new(); let mut language_ids = HashSet::new();
// Enable languages from `Accept-Language` header
for l in language_tags { for l in language_tags {
if let Some(found) = all_languages.iter().find(|all| all.code == l) { if let Some(found) = all_languages.iter().find(|all| all.code == l) {
language_ids.insert(found.id); 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 language_ids = language_ids.into_iter().collect();
let inserted_local_user = let inserted_local_user =
@ -483,7 +507,7 @@ async fn send_verification_email_if_required(
&local_user &local_user
.email .email
.clone() .clone()
.expect("invalid verification email"), .ok_or(LemmyErrorType::EmailRequired)?,
&mut context.pool(), &mut context.pool(),
context.settings(), context.settings(),
) )
@ -524,18 +548,16 @@ async fn oauth_request_access_token(
("client_secret", &oauth_provider.client_secret), ("client_secret", &oauth_provider.client_secret),
]) ])
.send() .send()
.await; .await
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?
let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?; .error_for_status()
if !response.status().is_success() { .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
Err(LemmyErrorType::OauthLoginFailed)?;
}
// Extract the access token // Extract the access token
let token_response = response let token_response = response
.json::<TokenResponse>() .json::<TokenResponse>()
.await .await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?; .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
Ok(token_response) Ok(token_response)
} }
@ -552,18 +574,16 @@ async fn oidc_get_user_info(
.header("Accept", "application/json") .header("Accept", "application/json")
.bearer_auth(access_token) .bearer_auth(access_token)
.send() .send()
.await; .await
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?
let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?; .error_for_status()
if !response.status().is_success() { .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
Err(LemmyErrorType::OauthLoginFailed)?;
}
// Extract the OAUTH user_id claim from the returned user_info // Extract the OAUTH user_id claim from the returned user_info
let user_info = response let user_info = response
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await .await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?; .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
Ok(user_info) 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> { fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult<String> {
if let Some(value) = user_info.get(key) { if let Some(value) = user_info.get(key) {
let result = serde_json::from_value::<String>(value.clone()) let result = serde_json::from_value::<String>(value.clone())
.map_err(|_| LemmyErrorType::OauthLoginFailed)?; .with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
return Ok(result); return Ok(result);
} }
Err(LemmyErrorType::OauthLoginFailed)? Err(LemmyErrorType::OauthLoginFailed)?

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,7 +42,7 @@ pub(crate) async fn send_activity_in_community(
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
// If community is local only, don't send anything out // If community is local only, don't send anything out
if community.visibility != CommunityVisibility::Public { if community.visibility == CommunityVisibility::LocalOnly {
return Ok(()); 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 = Person::read(&mut context.pool(), object_creator_id).await?;
let object_creator_site: Option<ApubSite> = let object_creator_site: Option<ApubSite> =
Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id) Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id)
.await? .await
.ok()
.map(Into::into); .map(Into::into);
if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) { if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) {
inboxes.add_inbox(inbox); inboxes.add_inbox(inbox);

View file

@ -17,6 +17,7 @@ use activitypub_federation::{
kinds::activity::UpdateType, kinds::activity::UpdateType,
traits::{ActivityHandler, Actor, Object}, traits::{ActivityHandler, Actor, Object},
}; };
use chrono::Utc;
use lemmy_api_common::context::LemmyContext; use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
@ -25,7 +26,6 @@ use lemmy_db_schema::{
person::Person, person::Person,
}, },
traits::Crud, traits::Crud,
utils::naive_now,
}; };
use lemmy_utils::error::{LemmyError, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url; use url::Url;
@ -103,7 +103,7 @@ impl ActivityHandler for UpdateCommunity {
nsfw: Some(self.object.sensitive.unwrap_or(false)), nsfw: Some(self.object.sensitive.unwrap_or(false)),
actor_id: Some(self.object.id.into()), actor_id: Some(self.object.id.into()),
public_key: Some(self.object.public_key.public_key_pem), 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())), icon: Some(self.object.icon.map(|i| i.url.into())),
banner: Some(self.object.image.map(|i| i.url.into())), banner: Some(self.object.image.map(|i| i.url.into())),
followers_url: self.object.followers.map(Into::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 // TODO: for compatibility with other projects, it would be much better to read this from cc or
// tags // tags
let mentions = scrape_text_for_mentions(&comment.content); 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?; send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?;
Ok(()) Ok(())
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,10 @@ use lemmy_api_common::{
post::{GetPosts, GetPostsResponse}, post::{GetPosts, GetPostsResponse},
utils::{check_conflicting_like_filters, check_private_instance}, 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::{ use lemmy_db_views::{
post_view::PostQuery, post_view::PostQuery,
structs::{LocalUserView, PaginationCursor, SiteView}, structs::{LocalUserView, PaginationCursor, SiteView},
@ -90,6 +93,17 @@ pub async fn list_posts(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntGetPosts)?; .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 // 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); let next_page = posts.last().map(PaginationCursor::after_post);
Ok(Json(GetPostsResponse { posts, next_page })) Ok(Json(GetPostsResponse { posts, next_page }))

View file

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

View file

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

View file

@ -6,28 +6,41 @@ use crate::{
community_moderators::ApubCommunityModerators, community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox, community_outbox::ApubCommunityOutbox,
}, },
fetcher::site_or_community_or_user::SiteOrCommunityOrUser,
http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response}, http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response},
objects::community::ApubCommunity, objects::community::ApubCommunity,
}; };
use activitypub_federation::{ use activitypub_federation::{
actix_web::signing_actor,
config::Data, config::Data,
fetch::object_id::ObjectId,
traits::{Collection, Object}, 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_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 lemmy_utils::error::{LemmyErrorType, LemmyResult};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub(crate) struct CommunityQuery { pub(crate) struct CommunityPath {
community_name: String, 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. /// Return the ActivityPub json representation of a local community over HTTP.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_community_http( pub(crate) async fn get_apub_community_http(
info: web::Path<CommunityQuery>, info: Path<CommunityPath>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
let community: ApubCommunity = 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). /// Returns an empty followers collection, only populating the size (for privacy).
pub(crate) async fn get_apub_community_followers( pub(crate) async fn get_apub_community_followers(
info: web::Path<CommunityQuery>, info: Path<CommunityPath>,
query: Query<CommunityIsFollowerQuery>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
let community = Community::read_from_name(&mut context.pool(), &info.community_name, false) let community = Community::read_from_name(&mut context.pool(), &info.community_name, false)
.await? .await?
.ok_or(LemmyErrorType::NotFound)?; .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)?; check_community_fetchable(&community)?;
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?; let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
create_apub_response(&followers) 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 /// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
/// activities like votes or comments). /// activities like votes or comments).
pub(crate) async fn get_apub_community_outbox( pub(crate) async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>, info: Path<CommunityPath>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
request: HttpRequest, request: HttpRequest,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
@ -77,7 +128,7 @@ pub(crate) async fn get_apub_community_outbox(
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_community_moderators( pub(crate) async fn get_apub_community_moderators(
info: web::Path<CommunityQuery>, info: Path<CommunityPath>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
let community: ApubCommunity = let community: ApubCommunity =
@ -92,7 +143,7 @@ pub(crate) async fn get_apub_community_moderators(
/// Returns collection of featured (stickied) posts. /// Returns collection of featured (stickied) posts.
pub(crate) async fn get_apub_community_featured( pub(crate) async fn get_apub_community_featured(
info: web::Path<CommunityQuery>, info: Path<CommunityPath>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
request: HttpRequest, request: HttpRequest,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
@ -181,17 +232,17 @@ pub(crate) mod tests {
let request = TestRequest::default().to_http_request(); let request = TestRequest::default().to_http_request();
// fetch invalid community // fetch invalid community
let query = CommunityQuery { let query = CommunityPath {
community_name: "asd".to_string(), community_name: "asd".to_string(),
}; };
let res = get_apub_community_http(query.into(), context.reset_request_count()).await; let res = get_apub_community_http(query.into(), context.reset_request_count()).await;
assert!(res.is_err()); assert!(res.is_err());
// fetch valid community // fetch valid community
let query = CommunityQuery { let path = CommunityPath {
community_name: community.name.clone(), 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()); assert_eq!(200, res.status());
let res_group: Group = decode_response(res).await?; let res_group: Group = decode_response(res).await?;
let community: ApubCommunity = community.into(); let community: ApubCommunity = community.into();
@ -199,20 +250,26 @@ pub(crate) mod tests {
assert_eq!(group, res_group); assert_eq!(group, res_group);
let res = get_apub_community_featured( 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(), context.reset_request_count(),
request.clone(), request.clone(),
) )
.await?; .await?;
assert_eq!(200, res.status()); assert_eq!(200, res.status());
let res = 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()); assert_eq!(200, res.status());
let res = let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?; get_apub_community_outbox(path.into(), context.reset_request_count(), request).await?;
assert_eq!(200, res.status());
let res =
get_apub_community_outbox(query.into(), context.reset_request_count(), request).await?;
assert_eq!(200, res.status()); assert_eq!(200, res.status());
Instance::delete(&mut context.pool(), instance.id).await?; Instance::delete(&mut context.pool(), instance.id).await?;
@ -227,28 +284,35 @@ pub(crate) mod tests {
let request = TestRequest::default().to_http_request(); let request = TestRequest::default().to_http_request();
// should return tombstone // should return tombstone
let query = CommunityQuery { let path: Path<CommunityPath> = CommunityPath {
community_name: community.name.clone(), 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()); assert_eq!(410, res.status());
let res_tombstone = decode_response::<Tombstone>(res).await; let res_tombstone = decode_response::<Tombstone>(res).await;
assert!(res_tombstone.is_ok()); assert!(res_tombstone.is_ok());
let res = get_apub_community_featured( 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(), context.reset_request_count(),
request.clone(), request.clone(),
) )
.await; .await;
assert!(res.is_err()); assert!(res.is_err());
let res = 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()); assert!(res.is_err());
let res = let res = get_apub_community_outbox(path, context.reset_request_count(), request).await;
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;
assert!(res.is_err()); assert!(res.is_err());
//Community::delete(&mut context.pool(), community.id).await?; //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 (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?;
let request = TestRequest::default().to_http_request(); let request = TestRequest::default().to_http_request();
let query = CommunityQuery { let path: Path<CommunityPath> = CommunityPath {
community_name: community.name.clone(), 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()); assert!(res.is_err());
let res = get_apub_community_featured( 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(), context.reset_request_count(),
request.clone(), request.clone(),
) )
.await; .await;
assert!(res.is_err()); assert!(res.is_err());
let res = 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()); assert!(res.is_err());
let res = let res = get_apub_community_outbox(path, context.reset_request_count(), request).await;
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;
assert!(res.is_err()); assert!(res.is_err());
Instance::delete(&mut context.pool(), instance.id).await?; Instance::delete(&mut context.pool(), instance.id).await?;

View file

@ -8,6 +8,7 @@ use activitypub_federation::{
actix_web::{inbox::receive_activity, signing_actor}, actix_web::{inbox::receive_activity, signing_actor},
config::Data, config::Data,
protocol::context::WithContext, protocol::context::WithContext,
traits::Actor,
FEDERATION_CONTENT_TYPE, FEDERATION_CONTENT_TYPE,
}; };
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
@ -18,7 +19,7 @@ use lemmy_db_schema::{
CommunityVisibility, CommunityVisibility,
}; };
use lemmy_db_views_actor::structs::CommunityFollowerView; 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 serde::{Deserialize, Serialize};
use std::{ops::Deref, time::Duration}; use std::{ops::Deref, time::Duration};
use tokio::time::timeout; use tokio::time::timeout;
@ -46,7 +47,7 @@ pub async fn shared_inbox(
// consider the activity broken and move on. // consider the activity broken and move on.
timeout(INCOMING_ACTIVITY_TIMEOUT, receive_fut) timeout(INCOMING_ACTIVITY_TIMEOUT, receive_fut)
.await .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 /// 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(); .into();
let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id) let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id)
.await .await
.map_err(|_| FederationError::CouldntFindActivity)?; .with_lemmy_type(FederationError::CouldntFindActivity.into())?;
let sensitive = activity.sensitive; let sensitive = activity.sensitive;
if sensitive { if sensitive {
@ -145,6 +146,7 @@ async fn check_community_content_fetchable(
// from the fetching instance then fetching is allowed // from the fetching instance then fetching is allowed
Private => { Private => {
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(request, None, context).await?; let signing_actor = signing_actor::<SiteOrCommunityOrUser>(request, None, context).await?;
if community.local {
Ok( Ok(
CommunityFollowerView::check_has_followers_from_instance( CommunityFollowerView::check_has_followers_from_instance(
community.id, community.id,
@ -153,6 +155,18 @@ async fn check_community_content_fetchable(
) )
.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::{ use lemmy_utils::{
error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}, error::{FederationError, LemmyError, LemmyErrorType, LemmyResult},
CacheLock,
CACHE_DURATION_FEDERATION, CACHE_DURATION_FEDERATION,
}; };
use moka::future::Cache; use moka::future::Cache;
@ -50,7 +51,8 @@ impl UrlVerifier for VerifyUrlData {
async fn verify(&self, url: &Url) -> Result<(), ActivityPubError> { async fn verify(&self, url: &Url) -> Result<(), ActivityPubError> {
let local_site_data = local_site_data_cached(&mut (&self.0).into()) let local_site_data = local_site_data_cached(&mut (&self.0).into())
.await .await
.expect("read local site data"); .map_err(|e| ActivityPubError::Other(format!("Cant read local site data: {e}")))?;
use FederationError::*; use FederationError::*;
check_apub_id_valid(url, &local_site_data).map_err(|err| match err { check_apub_id_valid(url, &local_site_data).map_err(|err| match err {
LemmyError { 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 // 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 // cache these values for a short time, which will already make a huge difference and ensures that
// changes take effect quickly. // changes take effect quickly.
static CACHE: LazyLock<Cache<(), Arc<LocalSiteData>>> = LazyLock::new(|| { static CACHE: CacheLock<Arc<LocalSiteData>> = LazyLock::new(|| {
Cache::builder() Cache::builder()
.max_capacity(1) .max_capacity(1)
.time_to_live(CACHE_DURATION_FEDERATION) .time_to_live(CACHE_DURATION_FEDERATION)
@ -176,10 +178,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
.domain() .domain()
.ok_or(FederationError::UrlWithoutDomain)? .ok_or(FederationError::UrlWithoutDomain)?
.to_string(); .to_string();
let local_instance = context let local_instance = context.settings().get_hostname_without_port()?;
.settings()
.get_hostname_without_port()
.expect("local hostname is valid");
if domain == local_instance { if domain == local_instance {
return Ok(()); return Ok(());
} }
@ -196,10 +195,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
.iter() .iter()
.map(|i| i.domain.clone()) .map(|i| i.domain.clone())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let local_instance = context let local_instance = context.settings().get_hostname_without_port()?;
.settings()
.get_hostname_without_port()
.expect("local hostname is valid");
allowed_and_local.push(local_instance); allowed_and_local.push(local_instance);
let domain = apub_id let domain = apub_id

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,6 @@ use lemmy_db_schema::{
private_message::{PrivateMessage, PrivateMessageInsertForm}, private_message::{PrivateMessage, PrivateMessageInsertForm},
}, },
traits::Crud, traits::Crud,
utils::naive_now,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{ use lemmy_utils::{
@ -161,7 +160,7 @@ impl Object for ApubPrivateMessage {
ap_id: Some(note.id.into()), ap_id: Some(note.id.into()),
local: Some(false), 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?; let pm = PrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?;
Ok(pm.into()) 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> impl<T, S: SelectableExpression<current_value>> Insertable<T> for ValuesFromSeries<S>
where 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 { fn values(self) -> Self::Values {
dsl::select(self).values() dsl::select(self).values()

View file

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

View file

@ -38,7 +38,7 @@ AS $a$
BEGIN BEGIN
EXECUTE replace($b$ EXECUTE replace($b$
-- When a thing gets a vote, update its aggregates and its creator's aggregates -- 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 BEGIN
WITH thing_diff AS ( UPDATE WITH thing_diff AS ( UPDATE
thing_aggregates AS a 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) 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 ( FROM (
SELECT 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 WHERE
a.thing_id = diff.thing_id a.thing_id = diff.thing_id
AND (diff.upvotes, diff.downvotes) != (0, 0) AND (diff.upvotes, diff.downvotes) != (0, 0)
@ -360,7 +361,7 @@ CREATE TRIGGER comment_count
-- Count subscribers for communities. -- Count subscribers for communities.
-- subscribers should be updated only when a local community is followed by a local or remote person. -- 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. -- 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 BEGIN
UPDATE UPDATE
community_aggregates AS a community_aggregates AS a
@ -368,10 +369,11 @@ BEGIN
subscribers = a.subscribers + diff.subscribers, subscribers_local = a.subscribers_local + diff.subscribers_local subscribers = a.subscribers + diff.subscribers, subscribers_local = a.subscribers_local + diff.subscribers_local
FROM ( FROM (
SELECT 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 FROM select_old_and_new_rows AS old_and_new_rows
LEFT JOIN community ON community.id = (community_follower).community_id LEFT JOIN community ON community.id = (community_actions).community_id
LEFT JOIN person ON person.id = (community_follower).person_id GROUP BY (community_follower).community_id) AS diff 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 WHERE
a.community_id = diff.community_id a.community_id = diff.community_id
AND (diff.subscribers, diff.subscribers_local) != (0, 0); 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. -- 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. -- Deleting rows and updating IDs are already handled by `CASCADE` in foreign key constraints.
CREATE FUNCTION r.comment_aggregates_from_comment () CREATE FUNCTION r.comment_aggregates_from_comment ()
@ -541,7 +581,7 @@ CREATE FUNCTION r.delete_follow_before_person ()
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
BEGIN BEGIN
DELETE FROM community_follower AS c DELETE FROM community_actions AS c
WHERE c.person_id = OLD.id; WHERE c.person_id = OLD.id;
RETURN OLD; RETURN OLD;
END; END;

View file

@ -151,3 +151,118 @@ DECLARE
END; END;
$a$; $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 inserted_post = Post::create(pool, &new_post).await?;
let post_like = PostLikeForm { let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1);
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let _inserted_post_like = PostLike::like(pool, &post_like).await?; let _inserted_post_like = PostLike::like(pool, &post_like).await?;
let comment_form = CommentInsertForm::new( let comment_form = CommentInsertForm::new(

View file

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

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