Merge remote-tracking branch 'upstream/main' into defaults

This commit is contained in:
dull b 2023-07-23 18:20:19 +00:00
commit 374b0f6f9e
169 changed files with 2693 additions and 1653 deletions

View file

@ -1,5 +1,5 @@
tab_spaces = 2
edition="2021"
imports_layout="HorizontalVertical"
imports_granularity="Crate"
group_imports="One"
edition = "2021"
imports_layout = "HorizontalVertical"
imports_granularity = "Crate"
group_imports = "One"

View file

@ -18,7 +18,6 @@ pipeline:
image: alpine:3
commands:
- apk add git
#- git fetch --tags
- git submodule init
- git submodule update
@ -27,7 +26,34 @@ pipeline:
commands:
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations'
# use minimum supported rust version for most steps
restore-cache:
image: meltwater/drone-cache:v1
pull: true
settings:
restore: true
endpoint:
from_secret: MINIO_ENDPOINT
access-key:
from_secret: MINIO_WRITE_USER
secret-key:
from_secret: MINIO_WRITE_PASSWORD
bucket:
from_secret: MINIO_BUCKET
region: us-east-1
cache_key: "rust-cache"
path-style: true
mount:
- ".cargo"
- "target"
- "api_tests/node_modules"
secrets:
[MINIO_ENDPOINT, MINIO_WRITE_USER, MINIO_WRITE_PASSWORD, MINIO_BUCKET]
toml_fmt:
image: tamasfe/taplo:0.8.1
commands:
- taplo format --check
cargo_fmt:
image: *muslrust_image
environment:
@ -35,42 +61,15 @@ pipeline:
CARGO_HOME: .cargo
commands:
# need make existing toolchain available
- cp ~/.cargo . -r
- rustup toolchain install nightly
- rustup component add rustfmt --toolchain nightly
- cargo +nightly fmt -- --check
# when:
# platform: linux/amd64
cargo_clippy:
image: *muslrust_image
environment:
CARGO_HOME: .cargo
commands:
# latest rust for clippy to get extra checks
# when adding new clippy lints, make sure to also add them in scripts/fix-clippy.sh
- rustup component add clippy
- cargo clippy --workspace --tests --all-targets --features console --
-D warnings -D deprecated -D clippy::perf -D clippy::complexity
-D clippy::style -D clippy::correctness -D clippy::suspicious
-D clippy::dbg_macro -D clippy::inefficient_to_string
-D clippy::items-after-statements -D clippy::implicit_clone
-D clippy::cast_lossless -D clippy::manual_string_new
-D clippy::redundant_closure_for_method_calls
-D clippy::unused_self
-A clippy::uninlined_format_args
-D clippy::get_first
-D clippy::explicit_into_iter_loop
-D clippy::explicit_iter_loop
-D clippy::needless_collect
- cargo clippy --workspace --features console --
-D clippy::unwrap_used
-D clippy::indexing_slicing
- cp -n ~/.cargo . -r
- rustup toolchain install nightly-2023-07-10
- rustup component add rustfmt --toolchain nightly-2023-07-10
- cargo +nightly-2023-07-10 fmt -- --check
# when:
# platform: linux/amd64
# make sure api builds with default features (used by other crates relying on lemmy api)
cargo_check:
check_api_common_default_features:
image: *muslrust_image
environment:
CARGO_HOME: .cargo
@ -88,6 +87,14 @@ pipeline:
# when:
# platform: linux/amd64
lemmy_api_common_works_with_wasm:
image: *muslrust_image
environment:
CARGO_HOME: .cargo
commands:
- "rustup target add wasm32-unknown-unknown"
- "cargo check --target wasm32-unknown-unknown -p lemmy_api_common"
check_defaults_hjson_updated:
image: *muslrust_image
environment:
@ -109,12 +116,45 @@ pipeline:
- diesel print-schema --config-file=diesel.toml > tmp.schema
- diff tmp.schema crates/db_schema/src/schema.rs
check_diesel_migration_revertable:
image: willsquire/diesel-cli
environment:
CARGO_HOME: .cargo
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands:
- diesel migration run
- diesel migration redo
cargo_clippy:
image: *muslrust_image
environment:
CARGO_HOME: .cargo
commands:
# when adding new clippy lints, make sure to also add them in scripts/fix-clippy.sh
- rustup component add clippy
- cargo clippy --workspace --tests --all-targets --features console --
-D warnings -D deprecated -D clippy::perf -D clippy::complexity
-D clippy::style -D clippy::correctness -D clippy::suspicious
-D clippy::dbg_macro -D clippy::inefficient_to_string
-D clippy::items-after-statements -D clippy::implicit_clone
-D clippy::cast_lossless -D clippy::manual_string_new
-D clippy::redundant_closure_for_method_calls
-D clippy::unused_self
-A clippy::uninlined_format_args
-D clippy::get_first
-D clippy::explicit_into_iter_loop
-D clippy::explicit_iter_loop
-D clippy::needless_collect
-D clippy::unwrap_used
-D clippy::indexing_slicing
# when:
# platform: linux/amd64
cargo_test:
image: *muslrust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
RUST_TEST_THREADS: "1"
CARGO_HOME: .cargo
commands:
- export LEMMY_CONFIG_LOCATION=../../config/config.hjson
@ -146,6 +186,29 @@ pipeline:
# when:
# platform: linux/amd64
rebuild-cache:
image: meltwater/drone-cache:v1
pull: true
settings:
rebuild: true
endpoint:
from_secret: MINIO_ENDPOINT
access-key:
from_secret: MINIO_WRITE_USER
secret-key:
from_secret: MINIO_WRITE_PASSWORD
bucket:
from_secret: MINIO_BUCKET
cache_key: "rust-cache"
region: us-east-1
path-style: true
mount:
- ".cargo"
- "target"
- "api_tests/node_modules"
secrets:
[MINIO_ENDPOINT, MINIO_WRITE_USER, MINIO_WRITE_PASSWORD, MINIO_BUCKET]
publish_release_docker:
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
@ -172,20 +235,6 @@ pipeline:
when:
event: cron
# using https://github.com/pksunkara/cargo-workspaces
publish_to_crates_io:
image: *muslrust_image
commands:
- 'echo "pub const VERSION: &str = \"$(git describe --tag)\";" > "crates/utils/src/version.rs"'
- cargo install cargo-workspaces
- cp -r migrations crates/db_schema/
- cargo login "$CARGO_API_TOKEN"
- cargo workspaces publish --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
secrets: [cargo_api_token]
when:
event: tag
#platform: linux/amd64
notify_on_failure:
image: alpine:3
commands:

56
Cargo.lock generated
View file

@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "activitypub_federation"
version = "0.4.5"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ab3ac148d9c0b4163a6d41040c17de7558a42224b9ecbd4e8f033aef6c254d9"
checksum = "4e6e7fefba6602240fcf612931b70640ad1e249dff833551ebc218f1c96a4193"
dependencies = [
"activitystreams-kinds",
"actix-web",
@ -22,7 +22,6 @@ dependencies = [
"bytes",
"chrono",
"derive_builder",
"displaydoc",
"dyn-clone",
"enum_delegate",
"futures-core",
@ -346,7 +345,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.10",
"once_cell",
"version_check",
]
@ -358,7 +357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom 0.2.8",
"getrandom 0.2.10",
"once_cell",
"version_check",
]
@ -678,7 +677,7 @@ checksum = "28d1c9c15093eb224f0baa400f38fcd713fc1391a6f1c389d886beef146d60a3"
dependencies = [
"base64 0.21.2",
"blowfish",
"getrandom 0.2.8",
"getrandom 0.2.10",
"subtle",
"zeroize",
]
@ -1548,17 +1547,6 @@ dependencies = [
"chrono",
]
[[package]]
name = "displaydoc"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.25",
]
[[package]]
name = "dlv-list"
version = "0.3.0"
@ -2045,9 +2033,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.8"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
@ -2397,17 +2385,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.3.0"
@ -2644,16 +2621,19 @@ dependencies = [
name = "lemmy_api_common"
version = "0.18.1"
dependencies = [
"activitypub_federation",
"actix-web",
"anyhow",
"chrono",
"encoding",
"futures",
"getrandom 0.2.10",
"lemmy_db_schema",
"lemmy_db_views",
"lemmy_db_views_actor",
"lemmy_db_views_moderator",
"lemmy_utils",
"once_cell",
"percent-encoding",
"regex",
"reqwest",
@ -2779,7 +2759,6 @@ dependencies = [
"tokio",
"tracing",
"ts-rs",
"typed-builder",
]
[[package]]
@ -2792,7 +2771,6 @@ dependencies = [
"serde",
"serde_with",
"ts-rs",
"typed-builder",
]
[[package]]
@ -3162,12 +3140,6 @@ dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "matchit"
version = "0.5.0"
@ -3195,7 +3167,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5736ba45bbac8f7ccc99a897f88ce85e508a18baec973a040f2514e6cdbff0d2"
dependencies = [
"idna 0.2.3",
"idna 0.3.0",
"once_cell",
"regex",
]
@ -4271,7 +4243,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.10",
]
[[package]]
@ -4437,7 +4409,7 @@ checksum = "1b97ad83c2fc18113346b7158d79732242002427c30f620fa817c1f32901e0a8"
dependencies = [
"anyhow",
"async-trait",
"getrandom 0.2.8",
"getrandom 0.2.10",
"matchit 0.7.0",
"opentelemetry 0.16.0",
"reqwest",
@ -5915,7 +5887,7 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.10",
"serde",
]

View file

@ -24,25 +24,36 @@ doctest = false
debug = 0
lto = "thin"
# This profile significantly speeds up build time. If debug info is needed you can comment the line
# out temporarily, but make sure to leave this in the main branch.
[profile.dev]
debug = 0
[features]
embed-pictrs = ["pict-rs"]
console = ["console-subscriber", "opentelemetry", "opentelemetry-otlp", "tracing-opentelemetry", "reqwest-tracing/opentelemetry_0_16"]
console = [
"console-subscriber",
"opentelemetry",
"opentelemetry-otlp",
"tracing-opentelemetry",
"reqwest-tracing/opentelemetry_0_16",
]
json-log = ["tracing-subscriber/json"]
prometheus-metrics = ["prometheus", "actix-web-prom"]
default = []
[workspace]
members = [
"crates/api",
"crates/api_crud",
"crates/api_common",
"crates/apub",
"crates/utils",
"crates/db_schema",
"crates/db_views",
"crates/db_views_actor",
"crates/db_views_actor",
"crates/routes"
"crates/api",
"crates/api_crud",
"crates/api_common",
"crates/apub",
"crates/utils",
"crates/db_schema",
"crates/db_views",
"crates/db_views_actor",
"crates/db_views_actor",
"crates/routes",
]
[workspace.dependencies]
@ -56,13 +67,21 @@ lemmy_routes = { version = "=0.18.1", path = "./crates/routes" }
lemmy_db_views = { version = "=0.18.1", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.18.1", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.18.1", path = "./crates/db_views_moderator" }
activitypub_federation = { version = "0.4.5", default-features = false, features = ["actix-web"] }
activitypub_federation = { version = "0.4.6", default-features = false, features = [
"actix-web",
] }
diesel = "2.1.0"
diesel_migrations = "2.1.0"
diesel-async = "0.3.1"
serde = { version = "1.0.167", features = ["derive"] }
serde_with = "3.0.0"
actix-web = { version = "4.3.1", default-features = false, features = ["macros", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] }
actix-web = { version = "4.3.1", default-features = false, features = [
"macros",
"rustls",
"compress-brotli",
"compress-gzip",
"compress-zstd",
] }
tracing = "0.1.37"
tracing-actix-web = { version = "0.7.5", default-features = false }
tracing-error = "0.2.0"
@ -82,7 +101,9 @@ base64 = "0.21.2"
uuid = { version = "1.4.0", features = ["serde", "v4"] }
async-trait = "0.1.71"
captcha = "0.0.9"
anyhow = { version = "1.0.71", features = ["backtrace"] } # backtrace is on by default on nightly, but not stable rust
anyhow = { version = "1.0.71", features = [
"backtrace",
] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.0"
typed-builder = "0.15.0"
serial_test = "2.0.0"
@ -91,7 +112,7 @@ sha2 = "0.10.7"
regex = "1.9.0"
once_cell = "1.18.0"
diesel-derive-newtype = "2.1.0"
diesel-derive-enum = {version = "2.1.0", features = ["postgres"] }
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = "0.25.0"
strum_macros = "0.25.1"
itertools = "0.11.0"
@ -103,7 +124,7 @@ rand = "0.8.5"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.19.0" }
ts-rs = { version = "6.2", features = ["serde-compat", "chrono-impl"] }
rustls = { version ="0.21.3", features = ["dangerous_configuration"]}
rustls = { version = "0.21.3", features = ["dangerous_configuration"] }
futures-util = "0.3.28"
tokio-postgres = "0.7.8"
tokio-postgres-rustls = "0.10.0"

View file

@ -16,7 +16,8 @@
<a href="readmes/README.es.md">Español</a> |
<a href="readmes/README.ru.md">Русский</a> |
<a href="readmes/README.zh.hans.md">汉语</a> |
<a href="readmes/README.zh.hant.md">漢語</a>
<a href="readmes/README.zh.hant.md">漢語</a> |
<a href="readmes/README.ja.md">日本語</a>
</p>
<p align="center">

View file

@ -470,7 +470,7 @@ The installation instructions have been slightly updated. However there are no b
Follow the upgrade instructions for [ansible](https://github.com/LemmyNet/lemmy-ansible#upgrading) or [docker](https://join-lemmy.org/docs/en/administration/install_docker.html#updating).
If you need help with the upgrade, you can ask in our [support forum](https://lemmy.ml/c/lemmy_support) or on the [Matrix Chat](https://matrix.to/#/!OwmdVYiZSXrXbtCNLw:matrix.org).
If you need help with the upgrade, you can ask in our [support forum](https://lemmy.ml/c/lemmy_support) or on the [Matrix Chat](https://matrix.to/#/#lemmy-admin-support-topics:discuss.online).
## Support development
@ -1016,8 +1016,8 @@ Next, **manually edit** your [lemmy.hjson](https://github.com/LemmyNet/lemmy/blo
- `pictrs_url` is removed, and the pictrs config is now a block. If using docker, it should look like:
```
pictrs: {
url: "http://pictrs:8080/"
# api_key: "API_KEY"
url: "http://pictrs:8080/"
api_key: "{{ postgres_password }}"
}
```
- The `rate_limit`, `federation`, `captcha`, and `slur_filter` blocks should be removed, as they are now in the database, and can be updated through the UI.
@ -1048,7 +1048,7 @@ _Note_: On production databases with thousands of comments, this upgrade **takes
_Note_: If you have any issues upgrading, you can restore your old database using the [backup and restore instructions here](https://join-lemmy.org/docs/en/administration/backup_and_restore.html).
If you need help with the upgrade, you can ask in our [support forum](https://lemmy.ml/c/lemmy_support) or on the [Matrix Chat](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org).
If you need help with the upgrade, you can ask in our [support forum](https://lemmy.ml/c/lemmy_support) or on the [Matrix Chat](https://matrix.to/#/#lemmy-admin-support-topics:discuss.online).
## Support development

View file

@ -19,7 +19,7 @@
"eslint": "^8.40.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^29.5.0",
"lemmy-js-client": "0.17.2-rc.13",
"lemmy-js-client": "0.18.3-rc.3",
"prettier": "^3.0.0",
"ts-jest": "^29.1.0",
"typescript": "^5.0.4"

View file

@ -1,11 +1,15 @@
#!/usr/bin/env bash
# IMPORTANT NOTE: this script does not use the normal LEMMY_DATABASE_URL format
# it is expected that this script is called by run-federation-test.sh script.
set -e
export RUST_BACKTRACE=1
export RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
echo "DB URL: ${LEMMY_DATABASE_URL} INSTANCE: $INSTANCE"
psql "${LEMMY_DATABASE_URL}/lemmy" -c "DROP DATABASE IF EXISTS $INSTANCE"
echo "create database"
psql "${LEMMY_DATABASE_URL}/lemmy" -c "CREATE DATABASE $INSTANCE"
done
@ -26,6 +30,7 @@ else
done
fi
echo "killall existing lemmy_server processes"
killall lemmy_server || true
echo "$PWD"
@ -59,7 +64,12 @@ target/lemmy_server >/tmp/lemmy_epsilon.out 2>&1 &
echo "wait for all instances to start"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-alpha:8541/api/v3/site')" != "200" ]]; do sleep 1; done
echo "alpha started"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-beta:8551/api/v3/site')" != "200" ]]; do sleep 1; done
echo "beta started"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-gamma:8561/api/v3/site')" != "200" ]]; do sleep 1; done
echo "gamma started"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-delta:8571/api/v3/site')" != "200" ]]; do sleep 1; done
echo "delta started"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-epsilon:8581/api/v3/site')" != "200" ]]; do sleep 1; done
echo "epsilon started. All started"

View file

@ -29,6 +29,7 @@ import {
getComments,
getCommentParentId,
resolveCommunity,
getPersonDetails,
} from "./shared";
import { CommentView } from "lemmy-js-client/dist/types/CommentView";
@ -82,8 +83,7 @@ test("Create a comment", async () => {
});
test("Create a comment in a non-existent post", async () => {
let commentRes = (await createComment(alpha, -1)) as any;
expect(commentRes.error).toBe("couldnt_find_post");
await expect(createComment(alpha, -1)).rejects.toBe("couldnt_find_post");
});
test("Update a comment", async () => {
@ -122,11 +122,9 @@ test("Delete a comment", async () => {
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
// Make sure that comment is undefined on beta
let betaCommentRes = (await resolveComment(
beta,
commentRes.comment_view.comment,
)) as any;
expect(betaCommentRes.error).toBe("couldnt_find_object");
await expect(
resolveComment(beta, commentRes.comment_view.comment),
).rejects.toBe("couldnt_find_object");
let undeleteCommentRes = await deleteComment(
alpha,
@ -160,9 +158,9 @@ test("Remove a comment from admin and community on the same instance", async ()
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
// Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
let refetchedPostComments = await getComments(
let refetchedPostComments = await getPersonDetails(
alpha,
postRes.post_view.post.id,
commentRes.comment_view.comment.creator_id,
);
expect(refetchedPostComments.comments[0].comment.removed).toBe(true);

View file

@ -52,8 +52,9 @@ test("Create community", async () => {
// A dupe check
let prevName = communityRes.community_view.community.name;
let communityRes2: any = await createCommunity(alpha, prevName);
expect(communityRes2["error"]).toBe("community_already_exists");
await expect(createCommunity(alpha, prevName)).rejects.toBe(
"community_already_exists",
);
// Cache the community on beta, make sure it has the other fields
let searchShort = `!${prevName}@lemmy-alpha:8541`;

View file

@ -88,17 +88,18 @@ test("Create a post", async () => {
assertPostFederation(betaPost, postRes.post_view);
// Delta only follows beta, so it should not see an alpha ap_id
let deltaPost = (await resolvePost(delta, postRes.post_view.post)).post;
expect(deltaPost).toBeUndefined();
await expect(resolvePost(delta, postRes.post_view.post)).rejects.toBe(
"couldnt_find_object",
);
// Epsilon has alpha blocked, it should not see the alpha post
let epsilonPost = (await resolvePost(epsilon, postRes.post_view.post)).post;
expect(epsilonPost).toBeUndefined();
await expect(resolvePost(epsilon, postRes.post_view.post)).rejects.toBe(
"couldnt_find_object",
);
});
test("Create a post in a non-existent community", async () => {
let postRes = (await createPost(alpha, -2)) as any;
expect(postRes.error).toBe("couldnt_find_community");
await expect(createPost(alpha, -2)).rejects.toBe("couldnt_find_community");
});
test("Unlike a post", async () => {
@ -145,8 +146,9 @@ test("Update a post", async () => {
assertPostFederation(betaPost, updatedPost.post_view);
// Make sure lemmy beta cannot update the post
let updatedPostBeta = (await editPost(beta, betaPost.post)) as any;
expect(updatedPostBeta.error).toBe("no_post_edit_allowed");
await expect(editPost(beta, betaPost.post)).rejects.toBe(
"no_post_edit_allowed",
);
});
test("Sticky a post", async () => {
@ -210,8 +212,7 @@ test("Lock a post", async () => {
expect(alphaPost1.post.locked).toBe(true);
// Try to make a new comment there, on alpha
let comment: any = await createComment(alpha, alphaPost1.post.id);
expect(comment["error"]).toBe("locked");
await expect(createComment(alpha, alphaPost1.post.id)).rejects.toBe("locked");
// Unlock a post
let unlockedPost = await lockPost(beta, false, betaPost1.post);
@ -242,9 +243,10 @@ test("Delete a post", async () => {
expect(deletedPost.post_view.post.name).toBe(postRes.post_view.post.name);
// Make sure lemmy beta sees post is deleted
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post;
// This will be undefined because of the tombstone
expect(betaPost).toBeUndefined();
await expect(resolvePost(beta, postRes.post_view.post)).rejects.toBe(
"couldnt_find_object",
);
// Undelete
let undeletedPost = await deletePost(alpha, false, postRes.post_view.post);
@ -259,8 +261,9 @@ test("Delete a post", async () => {
assertPostFederation(betaPost2, undeletedPost.post_view);
// Make sure lemmy beta cannot delete the post
let deletedPostBeta = (await deletePost(beta, true, betaPost2.post)) as any;
expect(deletedPostBeta.error).toStrictEqual("no_post_edit_allowed");
await expect(deletePost(beta, true, betaPost2.post)).rejects.toBe(
"no_post_edit_allowed",
);
});
test("Remove a post from admin and community on different instance", async () => {
@ -388,8 +391,8 @@ test("Enforce site ban for federated user", async () => {
expect(alphaUserOnBeta1.person?.person.banned).toBe(true);
// existing alpha post should be removed on beta
let searchBeta2 = await searchPostLocal(beta, postRes1.post_view.post);
expect(searchBeta2.posts[0].post.removed).toBe(true);
let searchBeta2 = await getPost(beta, searchBeta1.posts[0].post.id);
expect(searchBeta2.post_view.post.removed).toBe(true);
// Unban alpha
let unBanAlpha = await banPersonFromSite(
@ -436,12 +439,14 @@ test("Enforce community ban for federated user", async () => {
expect(banAlpha.banned).toBe(true);
// ensure that the post by alpha got removed
let searchAlpha1 = await searchPostLocal(alpha, postRes1.post_view.post);
expect(searchAlpha1.posts[0].post.removed).toBe(true);
await expect(getPost(alpha, searchBeta1.posts[0].post.id)).rejects.toBe(
"unknown",
);
// Alpha tries to make post on beta, but it fails because of ban
let postRes2 = await createPost(alpha, betaCommunity.community.id);
expect(postRes2.post_view).toBeUndefined();
await expect(createPost(alpha, betaCommunity.community.id)).rejects.toBe(
"banned_from_community",
);
// Unban alpha
let unBanAlpha = await banPersonFromCommunity(

View file

@ -58,6 +58,8 @@ import { CommentReportResponse } from "lemmy-js-client/dist/types/CommentReportR
import { CreateCommentReport } from "lemmy-js-client/dist/types/CreateCommentReport";
import { ListCommentReportsResponse } from "lemmy-js-client/dist/types/ListCommentReportsResponse";
import { ListCommentReports } from "lemmy-js-client/dist/types/ListCommentReports";
import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDetailsResponse";
import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails";
export interface API {
client: LemmyHttp;
@ -186,8 +188,11 @@ export async function setupLogins() {
await epsilon.client.editSite(editSiteForm);
// Create the main alpha/beta communities
await createCommunity(alpha, "main");
await createCommunity(beta, "main");
// Ignore thrown errors of duplicates
try {
await createCommunity(alpha, "main");
await createCommunity(beta, "main");
} catch (_) {}
}
export async function createPost(
@ -646,6 +651,16 @@ export async function saveUserSettings(
): Promise<LoginResponse> {
return api.client.saveUserSettings(form);
}
export async function getPersonDetails(
api: API,
person_id: number,
): Promise<GetPersonDetailsResponse> {
let form: GetPersonDetails = {
auth: api.auth,
person_id: person_id,
};
return api.client.getPersonDetails(form);
}
export async function deleteUser(api: API): Promise<DeleteAccountResponse> {
let form: DeleteAccount = {

View file

@ -92,10 +92,18 @@ test("Delete user", async () => {
await deleteUser(user);
expect((await resolvePost(alpha, localPost)).post).toBeUndefined();
expect((await resolveComment(alpha, localComment)).comment).toBeUndefined();
expect((await resolvePost(alpha, remotePost)).post).toBeUndefined();
expect((await resolveComment(alpha, remoteComment)).comment).toBeUndefined();
await expect(resolvePost(alpha, localPost)).rejects.toBe(
"couldnt_find_object",
);
await expect(resolveComment(alpha, localComment)).rejects.toBe(
"couldnt_find_object",
);
await expect(resolvePost(alpha, remotePost)).rejects.toBe(
"couldnt_find_object",
);
await expect(resolveComment(alpha, remoteComment)).rejects.toBe(
"couldnt_find_object",
);
});
test("Requests with invalid auth should be treated as unauthenticated", async () => {

View file

@ -2157,10 +2157,10 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
lemmy-js-client@0.17.2-rc.13:
version "0.17.2-rc.13"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.13.tgz#f2a61050c1308e85cb39c0e1f561e392e84e3921"
integrity sha512-4IyR1pisCumJ9L8fEPISC+Su1kVTI4pL/gWLsuOXxZC/lK36mG2+NfaNPiUmIklpCF5TUN+1F7E9bEvtTGogww==
lemmy-js-client@0.18.3-rc.3:
version "0.18.3-rc.3"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.18.3-rc.3.tgz#fc6489eb141bd09558bca38d9e46b40771a29f37"
integrity sha512-njixgXk4uMU4gGifnljwhSe9Kf445C4wAXcXhtpTtwPPLXpHQgxA1RASMb9Uq4zblfE6nC2JbrAka8y8N2N/Bw==
dependencies:
cross-fetch "^3.1.5"
form-data "^4.0.0"

View file

@ -22,24 +22,19 @@ impl Perform for ListCommentReports {
let data: &ListCommentReports = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let person_id = local_user_view.person.id;
let admin = local_user_view.person.admin;
let community_id = data.community_id;
let unresolved_only = data.unresolved_only;
let page = data.page;
let limit = data.limit;
let comment_reports = CommentReportQuery::builder()
.pool(&mut context.pool())
.my_person_id(person_id)
.admin(admin)
.community_id(community_id)
.unresolved_only(unresolved_only)
.page(page)
.limit(limit)
.build()
.list()
.await?;
let comment_reports = CommentReportQuery {
community_id,
unresolved_only,
page,
limit,
}
.list(&mut context.pool(), &local_user_view.person)
.await?;
Ok(ListCommentReportsResponse { comment_reports })
}

View file

@ -76,6 +76,9 @@ pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Resul
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use lemmy_api_common::utils::check_validator_time;
use lemmy_db_schema::{
source::{

View file

@ -27,18 +27,17 @@ impl Perform for GetPersonMentions {
let person_id = Some(local_user_view.person.id);
let show_bot_accounts = Some(local_user_view.local_user.show_bot_accounts);
let mentions = PersonMentionQuery::builder()
.pool(&mut context.pool())
.recipient_id(person_id)
.my_person_id(person_id)
.sort(sort)
.unread_only(unread_only)
.show_bot_accounts(show_bot_accounts)
.page(page)
.limit(limit)
.build()
.list()
.await?;
let mentions = PersonMentionQuery {
recipient_id: person_id,
my_person_id: person_id,
sort,
unread_only,
show_bot_accounts,
page,
limit,
}
.list(&mut context.pool())
.await?;
Ok(GetPersonMentionsResponse { mentions })
}

View file

@ -24,18 +24,17 @@ impl Perform for GetReplies {
let person_id = Some(local_user_view.person.id);
let show_bot_accounts = Some(local_user_view.local_user.show_bot_accounts);
let replies = CommentReplyQuery::builder()
.pool(&mut context.pool())
.recipient_id(person_id)
.my_person_id(person_id)
.sort(sort)
.unread_only(unread_only)
.show_bot_accounts(show_bot_accounts)
.page(page)
.limit(limit)
.build()
.list()
.await?;
let replies = CommentReplyQuery {
recipient_id: person_id,
my_person_id: person_id,
sort,
unread_only,
show_bot_accounts,
page,
limit,
}
.list(&mut context.pool())
.await?;
Ok(GetRepliesResponse { replies })
}

View file

@ -133,6 +133,7 @@ impl Perform for SaveUserSettings {
.totp_2fa_secret(totp_2fa_secret)
.totp_2fa_url(totp_2fa_url)
.open_links_in_new_tab(data.open_links_in_new_tab)
.infinite_scroll_enabled(data.infinite_scroll_enabled)
.build();
let local_user_res =

View file

@ -22,24 +22,19 @@ impl Perform for ListPostReports {
let data: &ListPostReports = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let person_id = local_user_view.person.id;
let admin = local_user_view.person.admin;
let community_id = data.community_id;
let unresolved_only = data.unresolved_only;
let page = data.page;
let limit = data.limit;
let post_reports = PostReportQuery::builder()
.pool(&mut context.pool())
.my_person_id(person_id)
.admin(admin)
.community_id(community_id)
.unresolved_only(unresolved_only)
.page(page)
.limit(limit)
.build()
.list()
.await?;
let post_reports = PostReportQuery {
community_id,
unresolved_only,
page,
limit,
}
.list(&mut context.pool(), &local_user_view.person)
.await?;
Ok(ListPostReportsResponse { post_reports })
}

View file

@ -21,14 +21,13 @@ impl Perform for ListPrivateMessageReports {
let unresolved_only = self.unresolved_only;
let page = self.page;
let limit = self.limit;
let private_message_reports = PrivateMessageReportQuery::builder()
.pool(&mut context.pool())
.unresolved_only(unresolved_only)
.page(page)
.limit(limit)
.build()
.list()
.await?;
let private_message_reports = PrivateMessageReportQuery {
unresolved_only,
page,
limit,
}
.list(&mut context.pool())
.await?;
Ok(ListPrivateMessageReportsResponse {
private_message_reports,

View file

@ -23,19 +23,18 @@ impl Perform for ListRegistrationApplications {
is_admin(&local_user_view)?;
let unread_only = data.unread_only;
let verified_email_only = local_site.require_email_verification;
let verified_email_only = Some(local_site.require_email_verification);
let page = data.page;
let limit = data.limit;
let registration_applications = RegistrationApplicationQuery::builder()
.pool(&mut context.pool())
.unread_only(unread_only)
.verified_email_only(Some(verified_email_only))
.page(page)
.limit(limit)
.build()
.list()
.await?;
let registration_applications = RegistrationApplicationQuery {
unread_only,
verified_email_only,
page,
limit,
}
.list(&mut context.pool())
.await?;
Ok(Self::Response {
registration_applications,

View file

@ -14,9 +14,27 @@ path = "src/lib.rs"
doctest = false
[features]
full = ["tracing", "rosetta-i18n", "chrono", "lemmy_utils",
"lemmy_db_views/full", "lemmy_db_views_actor/full", "lemmy_db_views_moderator/full",
"percent-encoding", "encoding", "reqwest-middleware", "webpage", "ts-rs"]
full = [
"tracing",
"rosetta-i18n",
"chrono",
"lemmy_utils",
"lemmy_db_views/full",
"lemmy_db_views_actor/full",
"lemmy_db_views_moderator/full",
"activitypub_federation",
"percent-encoding",
"encoding",
"reqwest-middleware",
"webpage",
"ts-rs",
"tokio",
"uuid",
"reqwest",
"actix-web",
"futures",
"once_cell",
]
[dependencies]
lemmy_db_views = { workspace = true }
@ -24,6 +42,7 @@ lemmy_db_views_moderator = { workspace = true }
lemmy_db_views_actor = { workspace = true }
lemmy_db_schema = { workspace = true }
lemmy_utils = { workspace = true, optional = true }
activitypub_federation = { workspace = true, optional = true }
serde = { workspace = true }
serde_with = { workspace = true }
url = { workspace = true }
@ -33,12 +52,17 @@ reqwest-middleware = { workspace = true, optional = true }
regex = { workspace = true }
rosetta-i18n = { workspace = true, optional = true }
percent-encoding = { workspace = true, optional = true }
webpage = { version = "1.6", default-features = false, features = ["serde"], optional = true }
webpage = { version = "1.6", default-features = false, features = [
"serde",
], optional = true }
encoding = { version = "0.2.33", optional = true }
anyhow = { workspace = true }
futures = { workspace = true }
uuid = { workspace = true }
tokio = { workspace = true }
reqwest = { workspace = true }
futures = { workspace = true, optional = true }
uuid = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true }
ts-rs = { workspace = true, optional = true }
actix-web = { workspace = true }
once_cell = { workspace = true, optional = true }
actix-web = { workspace = true, optional = true }
# necessary for wasmt compilation
getrandom = { version = "0.2.10", features = ["js"] }

View file

@ -64,7 +64,7 @@ pub async fn build_community_response(
}
pub async fn build_post_response(
context: &Data<LemmyContext>,
context: &LemmyContext,
community_id: CommunityId,
person_id: PersonId,
post_id: PostId,

View file

@ -10,6 +10,8 @@ pub mod post;
pub mod private_message;
#[cfg(feature = "full")]
pub mod request;
#[cfg(feature = "full")]
pub mod send_activity;
pub mod sensitive;
pub mod site;
#[cfg(feature = "full")]

View file

@ -133,6 +133,8 @@ pub struct SaveUserSettings {
pub auth: Sensitive<String>,
/// Open links in a new tab
pub open_links_in_new_tab: Option<bool>,
/// Enable infinite scroll
pub infinite_scroll_enabled: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]

View file

@ -270,6 +270,9 @@ pub fn build_user_agent(settings: &Settings) -> String {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::request::{
build_user_agent,
fetch_site_metadata,

View file

@ -0,0 +1,74 @@
use crate::context::LemmyContext;
use activitypub_federation::config::Data;
use futures::future::BoxFuture;
use lemmy_db_schema::source::post::Post;
use lemmy_utils::{error::LemmyResult, SYNCHRONOUS_FEDERATION};
use once_cell::sync::{Lazy, OnceCell};
use tokio::{
sync::{
mpsc,
mpsc::{UnboundedReceiver, UnboundedSender, WeakUnboundedSender},
Mutex,
},
task::JoinHandle,
};
type MatchOutgoingActivitiesBoxed =
Box<for<'a> fn(SendActivityData, &'a Data<LemmyContext>) -> BoxFuture<'a, LemmyResult<()>>>;
/// This static is necessary so that activities can be sent out synchronously for tests.
pub static MATCH_OUTGOING_ACTIVITIES: OnceCell<MatchOutgoingActivitiesBoxed> = OnceCell::new();
#[derive(Debug)]
pub enum SendActivityData {
CreatePost(Post),
}
// TODO: instead of static, move this into LemmyContext. make sure that stopping the process with
// ctrl+c still works.
static ACTIVITY_CHANNEL: Lazy<ActivityChannel> = Lazy::new(|| {
let (sender, receiver) = mpsc::unbounded_channel();
let weak_sender = sender.downgrade();
ActivityChannel {
weak_sender,
receiver: Mutex::new(receiver),
keepalive_sender: Mutex::new(Some(sender)),
}
});
pub struct ActivityChannel {
weak_sender: WeakUnboundedSender<SendActivityData>,
receiver: Mutex<UnboundedReceiver<SendActivityData>>,
keepalive_sender: Mutex<Option<UnboundedSender<SendActivityData>>>,
}
impl ActivityChannel {
pub async fn retrieve_activity() -> Option<SendActivityData> {
let mut lock = ACTIVITY_CHANNEL.receiver.lock().await;
lock.recv().await
}
pub async fn submit_activity(
data: SendActivityData,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
if *SYNCHRONOUS_FEDERATION {
MATCH_OUTGOING_ACTIVITIES
.get()
.expect("retrieve function pointer")(data, context)
.await?;
}
// could do `ACTIVITY_CHANNEL.keepalive_sender.lock()` instead and get rid of weak_sender,
// not sure which way is more efficient
else if let Some(sender) = ACTIVITY_CHANNEL.weak_sender.upgrade() {
sender.send(data)?;
}
Ok(())
}
pub async fn close(outgoing_activities_task: JoinHandle<LemmyResult<()>>) -> LemmyResult<()> {
ACTIVITY_CHANNEL.keepalive_sender.lock().await.take();
outgoing_activities_task.await??;
Ok(())
}
}

View file

@ -667,13 +667,13 @@ pub async fn remove_user_data_in_community(
// Comments
// TODO Diesel doesn't allow updates with joins, so this has to be a loop
let comments = CommentQuery::builder()
.pool(pool)
.creator_id(Some(banned_person_id))
.community_id(Some(community_id))
.build()
.list()
.await?;
let comments = CommentQuery {
creator_id: Some(banned_person_id),
community_id: Some(community_id),
..Default::default()
}
.list(pool)
.await?;
for comment_view in &comments {
let comment_id = comment_view.comment.id;
@ -731,6 +731,9 @@ pub async fn delete_user_account(
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::utils::{honeypot_check, password_length_check};
#[test]

View file

@ -23,4 +23,4 @@ url = { workspace = true }
async-trait = { workspace = true }
webmention = "0.4.0"
chrono = { workspace = true }
uuid = { workspace = true }
uuid = { workspace = true }

View file

@ -16,6 +16,7 @@ use lemmy_api_common::{
},
};
use lemmy_db_schema::{
impls::actor_language::default_post_language,
source::{
actor_language::CommunityLanguage,
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm},
@ -82,25 +83,31 @@ impl PerformCrud for CreateComment {
check_comment_depth(parent)?;
}
// if no language is set, copy language from parent post/comment
let parent_language = parent_opt
.as_ref()
.map(|p| p.language_id)
.unwrap_or(post.language_id);
let language_id = data.language_id.unwrap_or(parent_language);
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
Some(language_id),
data.language_id,
community_id,
)
.await?;
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => Some(lid),
None => {
default_post_language(
&mut context.pool(),
community_id,
local_user_view.local_user.id,
)
.await?
}
};
let comment_form = CommentInsertForm::builder()
.content(content_slurs_removed.clone())
.post_id(data.post_id)
.creator_id(local_user_view.person.id)
.language_id(Some(language_id))
.language_id(language_id)
.build();
// Create the comment

View file

@ -31,18 +31,18 @@ impl PerformCrud for ListCommunities {
let page = data.page;
let limit = data.limit;
let local_user = local_user_view.map(|l| l.local_user);
let communities = CommunityQuery::builder()
.pool(&mut context.pool())
.listing_type(listing_type)
.show_nsfw(show_nsfw)
.sort(sort)
.local_user(local_user.as_ref())
.page(page)
.limit(limit)
.is_mod_or_admin(is_admin)
.build()
.list()
.await?;
let communities = CommunityQuery {
listing_type,
show_nsfw,
sort,
local_user: local_user.as_ref(),
page,
limit,
is_mod_or_admin: is_admin,
..Default::default()
}
.list(&mut context.pool())
.await?;
// Return the jwt
Ok(ListCommunitiesResponse { communities })

View file

@ -5,7 +5,7 @@ use lemmy_utils::error::LemmyError;
mod comment;
mod community;
mod custom_emoji;
mod post;
pub mod post;
mod private_message;
mod site;
mod user;

View file

@ -1,10 +1,11 @@
use crate::PerformCrud;
use actix_web::web::Data;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
build_response::build_post_response,
context::LemmyContext,
post::{CreatePost, PostResponse},
request::fetch_site_data,
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_ban,
check_community_deleted_or_removed,
@ -40,147 +41,153 @@ use tracing::Instrument;
use url::Url;
use webmention::{Webmention, WebmentionError};
#[async_trait::async_trait(?Send)]
impl PerformCrud for CreatePost {
type Response = PostResponse;
#[tracing::instrument(skip(context))]
pub async fn create_post(
data: Json<CreatePost>,
context: Data<LemmyContext>,
) -> Result<Json<PostResponse>, LemmyError> {
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<PostResponse, LemmyError> {
let data: &CreatePost = self;
let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
let local_site = LocalSite::read(&mut context.pool()).await?;
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.name, &slur_regex)?;
check_slurs_opt(&data.body, &slur_regex)?;
honeypot_check(&data.honeypot)?;
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.name, &slur_regex)?;
check_slurs_opt(&data.body, &slur_regex)?;
honeypot_check(&data.honeypot)?;
let data_url = data.url.as_ref();
let url = data_url.map(clean_url_params).map(Into::into); // TODO no good way to handle a "clear"
let data_url = data.url.as_ref();
let url = data_url.map(clean_url_params).map(Into::into); // TODO no good way to handle a "clear"
is_valid_post_title(&data.name)?;
is_valid_body_field(&data.body, true)?;
check_url_scheme(&data.url)?;
is_valid_post_title(&data.name)?;
is_valid_body_field(&data.body, true)?;
check_url_scheme(&data.url)?;
check_community_ban(
local_user_view.person.id,
data.community_id,
&mut context.pool(),
)
.await?;
check_community_deleted_or_removed(data.community_id, &mut context.pool()).await?;
check_community_ban(
local_user_view.person.id,
data.community_id,
&mut context.pool(),
)
.await?;
check_community_deleted_or_removed(data.community_id, &mut context.pool()).await?;
let community_id = data.community_id;
let community = Community::read(&mut context.pool(), community_id).await?;
if community.posting_restricted_to_mods {
let community_id = data.community_id;
let community = Community::read(&mut context.pool(), community_id).await?;
if community.posting_restricted_to_mods {
let community_id = data.community_id;
let is_mod = CommunityView::is_mod_or_admin(
&mut context.pool(),
local_user_view.local_user.person_id,
community_id,
)
.await?;
if !is_mod {
return Err(LemmyErrorType::OnlyModsCanPostInCommunity)?;
}
}
// Fetch post links and pictrs cached image
let (metadata_res, thumbnail_url) =
fetch_site_data(context.client(), context.settings(), data_url, true).await;
let (embed_title, embed_description, embed_video_url) = metadata_res
.map(|u| (u.title, u.description, u.embed_video_url))
.unwrap_or_default();
let language_id = match data.language_id {
Some(lid) => Some(lid),
None => {
default_post_language(
&mut context.pool(),
community_id,
local_user_view.local_user.id,
)
.await?
}
};
CommunityLanguage::is_allowed_community_language(
let is_mod = CommunityView::is_mod_or_admin(
&mut context.pool(),
language_id,
local_user_view.local_user.person_id,
community_id,
)
.await?;
if !is_mod {
return Err(LemmyErrorType::OnlyModsCanPostInCommunity)?;
}
}
let post_form = PostInsertForm::builder()
.name(data.name.trim().to_owned())
.url(url)
.body(data.body.clone())
.community_id(data.community_id)
.creator_id(local_user_view.person.id)
.nsfw(data.nsfw)
.embed_title(embed_title)
.embed_description(embed_description)
.embed_video_url(embed_video_url)
.language_id(language_id)
.thumbnail_url(thumbnail_url)
.build();
// Fetch post links and pictrs cached image
let (metadata_res, thumbnail_url) =
fetch_site_data(context.client(), context.settings(), data_url, true).await;
let (embed_title, embed_description, embed_video_url) = metadata_res
.map(|u| (u.title, u.description, u.embed_video_url))
.unwrap_or_default();
let inserted_post = Post::create(&mut context.pool(), &post_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
// 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(),
data.language_id,
community_id,
)
.await?;
let inserted_post_id = inserted_post.id;
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
let apub_id = generate_local_apub_endpoint(
EndpointType::Post,
&inserted_post_id.to_string(),
&protocol_and_hostname,
)?;
let updated_post = Post::update(
&mut context.pool(),
inserted_post_id,
&PostUpdateForm::builder().ap_id(Some(apub_id)).build(),
)
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => Some(lid),
None => {
default_post_language(
&mut context.pool(),
community_id,
local_user_view.local_user.id,
)
.await?
}
};
let post_form = PostInsertForm::builder()
.name(data.name.trim().to_owned())
.url(url)
.body(data.body.clone())
.community_id(data.community_id)
.creator_id(local_user_view.person.id)
.nsfw(data.nsfw)
.embed_title(embed_title)
.embed_description(embed_description)
.embed_video_url(embed_video_url)
.language_id(language_id)
.thumbnail_url(thumbnail_url)
.build();
let inserted_post = Post::create(&mut context.pool(), &post_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
// They like their own post by default
let person_id = local_user_view.person.id;
let post_id = inserted_post.id;
let like_form = PostLikeForm {
post_id,
person_id,
score: 1,
};
let inserted_post_id = inserted_post.id;
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
let apub_id = generate_local_apub_endpoint(
EndpointType::Post,
&inserted_post_id.to_string(),
&protocol_and_hostname,
)?;
let updated_post = Post::update(
&mut context.pool(),
inserted_post_id,
&PostUpdateForm::builder().ap_id(Some(apub_id)).build(),
)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
PostLike::like(&mut context.pool(), &like_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
// They like their own post by default
let person_id = local_user_view.person.id;
let post_id = inserted_post.id;
let like_form = PostLikeForm {
post_id,
person_id,
score: 1,
};
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
PostLike::like(&mut context.pool(), &like_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
if let Some(url) = updated_post.url.clone() {
let task = async move {
let mut webmention =
Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
webmention.set_checked(true);
match webmention
.send()
.instrument(tracing::info_span!("Sending webmention"))
.await
{
Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()),
Ok(_) => Ok(()),
Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention),
}
};
if *SYNCHRONOUS_FEDERATION {
task.await?;
} else {
spawn_try_task(task);
ActivityChannel::submit_activity(SendActivityData::CreatePost(updated_post.clone()), &context)
.await?;
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
if let Some(url) = updated_post.url.clone() {
let task = async move {
let mut webmention =
Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
webmention.set_checked(true);
match webmention
.send()
.instrument(tracing::info_span!("Sending webmention"))
.await
{
Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()),
Ok(_) => Ok(()),
Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention),
}
};
if *SYNCHRONOUS_FEDERATION {
task.await?;
} else {
spawn_try_task(task);
}
};
build_post_response(context, community_id, person_id, post_id).await
}
Ok(Json(
build_post_response(&context, community_id, person_id, post_id).await?,
))
}

View file

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

View file

@ -100,12 +100,12 @@ impl PerformCrud for GetPost {
// Fetch the cross_posts
let cross_posts = if let Some(url) = &post_view.post.url {
let mut x_posts = PostQuery::builder()
.pool(&mut context.pool())
.url_search(Some(url.inner().as_str().into()))
.build()
.list()
.await?;
let mut x_posts = PostQuery {
url_search: Some(url.inner().as_str().into()),
..Default::default()
}
.list(&mut context.pool())
.await?;
// Don't return this post as one of the cross_posts
x_posts.retain(|x| x.post.id != post_id);

View file

@ -24,15 +24,13 @@ impl PerformCrud for GetPrivateMessages {
let page = data.page;
let limit = data.limit;
let unread_only = data.unread_only;
let mut messages = PrivateMessageQuery::builder()
.pool(&mut context.pool())
.recipient_id(person_id)
.page(page)
.limit(limit)
.unread_only(unread_only)
.build()
.list()
.await?;
let mut messages = PrivateMessageQuery {
page,
limit,
unread_only,
}
.list(&mut context.pool(), person_id)
.await?;
// Messages sent by ourselves should be marked as read. The `read` column in database is only
// for the recipient, and shouldnt be exposed to sender.

View file

@ -183,6 +183,9 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::site::create::validate_create_payload;
use lemmy_api_common::site::CreateSite;
use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode};

View file

@ -42,6 +42,9 @@ pub fn application_question_check(
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::site::{application_question_check, site_default_post_listing_type_check};
use lemmy_db_schema::{ListingType, RegistrationMode};

View file

@ -217,6 +217,9 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::site::update::validate_update_payload;
use lemmy_api_common::site::EditSite;
use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode};

View file

@ -138,6 +138,7 @@ impl PerformCrud for Register {
.password_encrypted(data.password.to_string())
.show_nsfw(Some(data.show_nsfw))
.accepted_application(accepted_application)
.default_listing_type(Some(local_site.default_post_listing_type))
.build();
let inserted_local_user = LocalUser::create(&mut context.pool(), &local_user_form).await?;

View file

@ -25,7 +25,7 @@ chrono = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
actix-web = { workspace = true }
tokio = {workspace = true}
tokio = { workspace = true }
tracing = { workspace = true }
strum_macros = { workspace = true }
url = { workspace = true }

View file

@ -9,7 +9,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
objects::{instance::remote_instance_inboxes, person::ApubPerson},
protocol::activities::block::block_user::BlockUser,
};
@ -124,6 +124,7 @@ impl ActivityHandler for BlockUser {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
match self.target.dereference(context).await? {
SiteOrCommunity::Site(site) => {
@ -147,7 +148,6 @@ impl ActivityHandler for BlockUser {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let expires = self.expires.map(|u| u.naive_local());
let mod_person = self.actor.dereference(context).await?;
let blocked_person = self.object.dereference(context).await?;

View file

@ -7,7 +7,7 @@ use crate::{
verify_is_public,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
objects::{instance::remote_instance_inboxes, person::ApubPerson},
protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
};
@ -88,6 +88,7 @@ impl ActivityHandler for UndoBlockUser {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
verify_domains_match(self.actor.inner(), self.object.actor.inner())?;
self.object.verify(context).await?;
@ -96,7 +97,6 @@ impl ActivityHandler for UndoBlockUser {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let expires = self.object.expires.map(|u| u.naive_local());
let mod_person = self.actor.dereference(context).await?;
let blocked_person = self.object.object.dereference(context).await?;

View file

@ -6,7 +6,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
objects::community::ApubCommunity,
protocol::{
activities::community::announce::{AnnounceActivity, RawAnnouncableActivities},
@ -133,14 +133,14 @@ impl ActivityHandler for AnnounceActivity {
}
#[tracing::instrument(skip_all)]
async fn verify(&self, _context: &Data<Self::DataType>) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let object: AnnouncableActivities = self.object.object(context).await?.try_into()?;
// This is only for sending, not receiving so we reject it.
if let AnnouncableActivities::Page(_) = object {

View file

@ -7,7 +7,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{
activities::community::{collection_add::CollectionAdd, collection_remove::CollectionRemove},
@ -108,6 +108,7 @@ impl ActivityHandler for CollectionAdd {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
@ -117,7 +118,6 @@ impl ActivityHandler for CollectionAdd {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let (community, collection_type) =
Community::get_by_collection_url(&mut context.pool(), &self.target.into()).await?;
match collection_type {

View file

@ -7,7 +7,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{activities::community::collection_remove::CollectionRemove, InCommunity},
};
@ -101,6 +101,7 @@ impl ActivityHandler for CollectionRemove {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
@ -110,7 +111,6 @@ impl ActivityHandler for CollectionRemove {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let (community, collection_type) =
Community::get_by_collection_url(&mut context.pool(), &self.target.into()).await?;
match collection_type {

View file

@ -8,7 +8,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
protocol::{
activities::community::lock_page::{LockPage, LockType, UndoLockPage},
InCommunity,
@ -79,6 +79,7 @@ impl ActivityHandler for UndoLockPage {
}
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
@ -94,7 +95,6 @@ impl ActivityHandler for UndoLockPage {
}
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
insert_activity(&self.id, &self, false, false, context).await?;
let form = PostUpdateForm::builder().locked(Some(false)).build();
let post = self.object.object.dereference(context).await?;
Post::update(&mut context.pool(), post.id, &form).await?;

View file

@ -1,6 +1,6 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person_in_community},
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::community::report::Report, InCommunity},
PostOrComment,
@ -115,6 +115,7 @@ impl ActivityHandler for Report {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
Ok(())
@ -122,7 +123,6 @@ impl ActivityHandler for Report {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let actor = self.actor.dereference(context).await?;
match self.object.dereference(context).await? {
PostOrComment::Post(post) => {

View file

@ -7,7 +7,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::community::update::UpdateCommunity, InCommunity},
SendActivity,
@ -82,6 +82,7 @@ impl ActivityHandler for UpdateCommunity {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
@ -92,7 +93,6 @@ impl ActivityHandler for UpdateCommunity {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let community = self.community(context).await?;
let community_update_form = self.object.into_update_form();

View file

@ -7,7 +7,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
mentions::MentionOrValue,
objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson},
protocol::{
@ -154,6 +154,7 @@ impl ActivityHandler for CreateOrUpdateNote {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
let post = self.object.get_parents(context).await?.0;
let community = self.community(context).await?;
@ -169,7 +170,6 @@ impl ActivityHandler for CreateOrUpdateNote {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
// Need to do this check here instead of Note::from_json because we need the person who
// send the activity, not the comment author.
let existing_comment = self.object.id.dereference_local(context).await.ok();

View file

@ -8,7 +8,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{
activities::{create_or_update::page::CreateOrUpdatePage, CreateOrUpdateType},
@ -24,7 +24,7 @@ use activitypub_federation::{
};
use lemmy_api_common::{
context::LemmyContext,
post::{CreatePost, EditPost, PostResponse},
post::{EditPost, PostResponse},
};
use lemmy_db_schema::{
aggregates::structs::PostAggregates,
@ -39,25 +39,6 @@ use lemmy_db_schema::{
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use url::Url;
#[async_trait::async_trait]
impl SendActivity for CreatePost {
type Response = PostResponse;
async fn send_activity(
_request: &Self,
response: &Self::Response,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdatePage::send(
&response.post_view.post,
response.post_view.creator.id,
CreateOrUpdateType::Create,
context,
)
.await
}
}
#[async_trait::async_trait]
impl SendActivity for EditPost {
type Response = PostResponse;
@ -68,10 +49,10 @@ impl SendActivity for EditPost {
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdatePage::send(
&response.post_view.post,
response.post_view.post.clone(),
response.post_view.creator.id,
CreateOrUpdateType::Update,
context,
context.reset_request_count(),
)
.await
}
@ -102,12 +83,12 @@ impl CreateOrUpdatePage {
#[tracing::instrument(skip_all)]
pub(crate) async fn send(
post: &Post,
post: Post,
person_id: PersonId,
kind: CreateOrUpdateType,
context: &Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<(), LemmyError> {
let post = ApubPost(post.clone());
let post = ApubPost(post);
let community_id = post.community_id;
let person: ApubPerson = Person::read(&mut context.pool(), person_id).await?.into();
let community: ApubCommunity = Community::read(&mut context.pool(), community_id)
@ -115,8 +96,8 @@ impl CreateOrUpdatePage {
.into();
let create_or_update =
CreateOrUpdatePage::new(post, &person, &community, kind, context).await?;
let is_mod_action = create_or_update.object.is_mod_action(context).await?;
CreateOrUpdatePage::new(post, &person, &community, kind, &context).await?;
let is_mod_action = create_or_update.object.is_mod_action(&context).await?;
let activity = AnnouncableActivities::CreateOrUpdatePost(create_or_update);
send_activity_in_community(
activity,
@ -124,7 +105,7 @@ impl CreateOrUpdatePage {
&community,
vec![],
is_mod_action,
context,
&context,
)
.await?;
Ok(())
@ -146,6 +127,7 @@ impl ActivityHandler for CreateOrUpdatePage {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
@ -180,7 +162,6 @@ impl ActivityHandler for CreateOrUpdatePage {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let post = ApubPost::from_json(self.object, context).await?;
// author likes their own post by default

View file

@ -1,6 +1,6 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person},
insert_activity,
insert_received_activity,
objects::{person::ApubPerson, private_message::ApubPrivateMessage},
protocol::activities::{
create_or_update::chat_message::CreateOrUpdateChatMessage,
@ -109,6 +109,7 @@ impl ActivityHandler for CreateOrUpdateChatMessage {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_person(&self.actor, context).await?;
verify_domains_match(self.actor.inner(), self.object.id.inner())?;
verify_domains_match(self.to[0].inner(), self.object.to[0].inner())?;
@ -118,7 +119,6 @@ impl ActivityHandler for CreateOrUpdateChatMessage {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
ApubPrivateMessage::from_json(self.object, context).await?;
Ok(())
}

View file

@ -3,7 +3,7 @@ use crate::{
deletion::{receive_delete_action, verify_delete_activity, DeletableObjects},
generate_activity_id,
},
insert_activity,
insert_received_activity,
objects::person::ApubPerson,
protocol::{activities::deletion::delete::Delete, IdOrNestedObject},
};
@ -43,13 +43,13 @@ impl ActivityHandler for Delete {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_delete_activity(self, self.summary.is_some(), context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
if let Some(reason) = self.summary {
// We set reason to empty string if it doesn't exist, to distinguish between delete and
// remove. Here we change it back to option, so we don't write it to db.

View file

@ -1,6 +1,6 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_is_public, verify_person},
insert_activity,
insert_received_activity,
objects::{instance::remote_instance_inboxes, person::ApubPerson},
protocol::activities::deletion::delete_user::DeleteUser,
SendActivity,
@ -73,6 +73,7 @@ impl ActivityHandler for DeleteUser {
}
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_is_public(&self.to, &[])?;
verify_person(&self.actor, context).await?;
verify_urls_match(self.actor.inner(), self.object.inner())?;
@ -80,7 +81,6 @@ impl ActivityHandler for DeleteUser {
}
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let actor = self.actor.dereference(context).await?;
delete_user_account(
actor.id,

View file

@ -3,7 +3,7 @@ use crate::{
deletion::{receive_delete_action, verify_delete_activity, DeletableObjects},
generate_activity_id,
},
insert_activity,
insert_received_activity,
objects::person::ApubPerson,
protocol::activities::deletion::{delete::Delete, undo_delete::UndoDelete},
};
@ -42,6 +42,7 @@ impl ActivityHandler for UndoDelete {
}
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
insert_received_activity(&self.id, data).await?;
self.object.verify(data).await?;
verify_delete_activity(&self.object, self.object.summary.is_some(), data).await?;
Ok(())
@ -49,7 +50,6 @@ impl ActivityHandler for UndoDelete {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
if self.object.summary.is_some() {
UndoDelete::receive_undo_remove_action(
&self.actor.dereference(context).await?,

View file

@ -1,6 +1,6 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity},
insert_activity,
insert_received_activity,
protocol::activities::following::{accept::AcceptFollow, follow::Follow},
};
use activitypub_federation::{
@ -50,6 +50,7 @@ impl ActivityHandler for AcceptFollow {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_urls_match(self.actor.inner(), self.object.object.inner())?;
self.object.verify(context).await?;
if let Some(to) = &self.to {
@ -60,7 +61,6 @@ impl ActivityHandler for AcceptFollow {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let community = self.actor.dereference(context).await?;
let person = self.object.actor.dereference(context).await?;
// This will throw an error if no follow was requested

View file

@ -6,7 +6,7 @@ use crate::{
verify_person_in_community,
},
fetcher::user_or_community::UserOrCommunity,
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::following::{
accept::AcceptFollow,
@ -90,6 +90,7 @@ impl ActivityHandler for Follow {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_person(&self.actor, context).await?;
let object = self.object.dereference(context).await?;
if let UserOrCommunity::Community(c) = object {
@ -103,7 +104,6 @@ impl ActivityHandler for Follow {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let actor = self.actor.dereference(context).await?;
let object = self.object.dereference(context).await?;
match object {

View file

@ -1,7 +1,7 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person},
fetcher::user_or_community::UserOrCommunity,
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::following::{follow::Follow, undo_follow::UndoFollow},
};
@ -60,6 +60,7 @@ impl ActivityHandler for UndoFollow {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
verify_urls_match(self.actor.inner(), self.object.actor.inner())?;
verify_person(&self.actor, context).await?;
self.object.verify(context).await?;
@ -71,7 +72,6 @@ impl ActivityHandler for UndoFollow {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let person = self.actor.dereference(context).await?;
let object = self.object.object.dereference(context).await?;

View file

@ -1,6 +1,6 @@
use crate::{
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::{create_or_update::page::CreateOrUpdatePage, CreateOrUpdateType},
CONTEXT,
};
use activitypub_federation::{
@ -12,12 +12,28 @@ use activitypub_federation::{
traits::{ActivityHandler, Actor},
};
use anyhow::anyhow;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{newtypes::CommunityId, source::community::Community};
use lemmy_api_common::{
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
};
use lemmy_db_schema::{
newtypes::CommunityId,
source::{
activity::{SentActivity, SentActivityForm},
community::Community,
instance::Instance,
},
};
use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
spawn_try_task,
SYNCHRONOUS_FEDERATION,
};
use moka::future::Cache;
use once_cell::sync::Lazy;
use serde::Serialize;
use std::ops::Deref;
use std::{ops::Deref, sync::Arc, time::Duration};
use tracing::info;
use url::{ParseError, Url};
use uuid::Uuid;
@ -30,6 +46,10 @@ pub mod following;
pub mod unfederated;
pub mod voting;
/// Amount of time that the list of dead instances is cached. This is only updated once a day,
/// so there is no harm in caching it for a longer time.
pub static DEAD_INSTANCE_LIST_CACHE_DURATION: Duration = Duration::from_secs(30 * 60);
/// Checks that the specified Url actually identifies a Person (by fetching it), and that the person
/// doesn't have a site ban.
#[tracing::instrument(skip_all)]
@ -148,7 +168,7 @@ async fn send_lemmy_activity<Activity, ActorT>(
data: &Data<LemmyContext>,
activity: Activity,
actor: &ActorT,
inbox: Vec<Url>,
mut inbox: Vec<Url>,
sensitive: bool,
) -> Result<(), LemmyError>
where
@ -156,11 +176,62 @@ where
ActorT: Actor,
Activity: ActivityHandler<Error = LemmyError>,
{
static CACHE: Lazy<Cache<(), Arc<Vec<String>>>> = Lazy::new(|| {
Cache::builder()
.max_capacity(1)
.time_to_live(DEAD_INSTANCE_LIST_CACHE_DURATION)
.build()
});
let dead_instances = CACHE
.try_get_with((), async {
Ok::<_, diesel::result::Error>(Arc::new(Instance::dead_instances(&mut data.pool()).await?))
})
.await?;
inbox.retain(|i| {
let domain = i.domain().expect("has domain").to_string();
!dead_instances.contains(&domain)
});
info!("Sending activity {}", activity.id().to_string());
let activity = WithContext::new(activity, CONTEXT.deref().clone());
insert_activity(activity.id(), &activity, true, sensitive, data).await?;
let form = SentActivityForm {
ap_id: activity.id().clone().into(),
data: serde_json::to_value(activity.clone())?,
sensitive,
};
SentActivity::create(&mut data.pool(), form).await?;
send_activity(activity, actor, inbox, data).await?;
Ok(())
}
pub async fn handle_outgoing_activities(context: Data<LemmyContext>) -> LemmyResult<()> {
while let Some(data) = ActivityChannel::retrieve_activity().await {
match_outgoing_activities(data, &context.reset_request_count()).await?
}
Ok(())
}
pub async fn match_outgoing_activities(
data: SendActivityData,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let fed_task = match data {
SendActivityData::CreatePost(post) => {
let creator_id = post.creator_id;
CreateOrUpdatePage::send(
post,
creator_id,
CreateOrUpdateType::Create,
context.reset_request_count(),
)
}
};
if *SYNCHRONOUS_FEDERATION {
fed_task.await?;
} else {
spawn_try_task(fed_task);
}
Ok(())
}

View file

@ -4,7 +4,7 @@ use crate::{
verify_person_in_community,
voting::{undo_vote_comment, undo_vote_post},
},
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{
activities::voting::{undo_vote::UndoVote, vote::Vote},
@ -57,6 +57,7 @@ impl ActivityHandler for UndoVote {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_urls_match(self.actor.inner(), self.object.actor.inner())?;
@ -66,7 +67,6 @@ impl ActivityHandler for UndoVote {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let actor = self.actor.dereference(context).await?;
let object = self.object.object.dereference(context).await?;
match object {

View file

@ -4,7 +4,7 @@ use crate::{
verify_person_in_community,
voting::{vote_comment, vote_post},
},
insert_activity,
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{
activities::voting::vote::{Vote, VoteType},
@ -56,6 +56,7 @@ impl ActivityHandler for Vote {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
let enable_downvotes = LocalSite::read(&mut context.pool())
@ -70,7 +71,6 @@ impl ActivityHandler for Vote {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let actor = self.actor.dereference(context).await?;
let object = self.object.dereference(context).await?;
match object {

View file

@ -134,6 +134,9 @@ impl InCommunity for AnnouncableActivities {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
activity_lists::{
GroupInboxActivities,

View file

@ -39,7 +39,11 @@ pub async fn list_comments(
let limit = data.limit;
let parent_id = data.parent_id;
let listing_type = listing_type_with_default(data.type_, &local_site, community_id)?;
let listing_type = Some(listing_type_with_default(
data.type_,
&local_site,
community_id,
)?);
// If a parent_id is given, fetch the comment to get the path
let parent_path = if let Some(parent_id) = parent_id {
@ -50,23 +54,22 @@ pub async fn list_comments(
let parent_path_cloned = parent_path.clone();
let post_id = data.post_id;
let local_user = local_user_view.map(|l| l.local_user);
let comments = CommentQuery::builder()
.pool(&mut context.pool())
.listing_type(Some(listing_type))
.sort(sort)
.max_depth(max_depth)
.saved_only(saved_only)
.community_id(community_id)
.parent_path(parent_path_cloned)
.post_id(post_id)
.local_user(local_user.as_ref())
.page(page)
.limit(limit)
.build()
.list()
.await
.with_lemmy_type(LemmyErrorType::CouldntGetComments)?;
let comments = CommentQuery {
listing_type,
sort,
max_depth,
saved_only,
community_id,
parent_path: parent_path_cloned,
post_id,
local_user: local_user_view.as_ref(),
page,
limit,
..Default::default()
}
.list(&mut context.pool())
.await
.with_lemmy_type(LemmyErrorType::CouldntGetComments)?;
Ok(Json(GetCommentsResponse { comments }))
}

View file

@ -8,7 +8,7 @@ use actix_web::web::{Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{GetPosts, GetPostsResponse},
utils::{check_private_instance, is_mod_or_admin_opt, local_user_view_from_jwt_opt},
utils::{check_private_instance, local_user_view_from_jwt_opt},
};
use lemmy_db_schema::source::{community::Community, local_site::LocalSite};
use lemmy_db_views::post_view::PostQuery;
@ -36,27 +36,25 @@ pub async fn list_posts(
};
let saved_only = data.saved_only;
let listing_type = listing_type_with_default(data.type_, &local_site, community_id)?;
let listing_type = Some(listing_type_with_default(
data.type_,
&local_site,
community_id,
)?);
let is_mod_or_admin =
is_mod_or_admin_opt(&mut context.pool(), local_user_view.as_ref(), community_id)
.await
.is_ok();
let posts = PostQuery::builder()
.pool(&mut context.pool())
.local_user(local_user_view.map(|l| l.local_user).as_ref())
.listing_type(Some(listing_type))
.sort(sort)
.community_id(community_id)
.saved_only(saved_only)
.page(page)
.limit(limit)
.is_mod_or_admin(Some(is_mod_or_admin))
.build()
.list()
.await
.with_lemmy_type(LemmyErrorType::CouldntGetPosts)?;
let posts = PostQuery {
local_user: local_user_view.as_ref(),
listing_type,
sort,
community_id,
saved_only,
page,
limit,
..Default::default()
}
.list(&mut context.pool())
.await
.with_lemmy_type(LemmyErrorType::CouldntGetPosts)?;
Ok(Json(GetPostsResponse { posts }))
}

View file

@ -4,7 +4,7 @@ use actix_web::web::{Json, Query};
use lemmy_api_common::{
context::LemmyContext,
person::{GetPersonDetails, GetPersonDetailsResponse},
utils::{check_private_instance, is_admin, local_user_view_from_jwt_opt},
utils::{check_private_instance, local_user_view_from_jwt_opt},
};
use lemmy_db_schema::{
source::{local_site::LocalSite, person::Person},
@ -26,7 +26,6 @@ pub async fn read_person(
let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
let local_site = LocalSite::read(&mut context.pool()).await?;
let is_admin = local_user_view.as_ref().map(|luv| is_admin(luv).is_ok());
check_private_instance(&local_user_view, &local_site)?;
@ -53,52 +52,42 @@ pub async fn read_person(
let limit = data.limit;
let saved_only = data.saved_only;
let community_id = data.community_id;
let local_user = local_user_view.map(|l| l.local_user);
let local_user_clone = local_user.clone();
// If its saved only, you don't care what creator it was
// Or, if its not saved, then you only want it for that specific creator
let creator_id = if !saved_only.unwrap_or(false) {
Some(person_details_id)
} else {
None
};
let posts = PostQuery::builder()
.pool(&mut context.pool())
.sort(sort)
.saved_only(saved_only)
.local_user(local_user.as_ref())
.community_id(community_id)
.is_mod_or_admin(is_admin)
.page(page)
.limit(limit)
.creator_id(
// If its saved only, you don't care what creator it was
// Or, if its not saved, then you only want it for that specific creator
if !saved_only.unwrap_or(false) {
Some(person_details_id)
} else {
None
},
)
.build()
.list()
.await?;
let posts = PostQuery {
sort,
saved_only,
local_user: local_user_view.as_ref(),
community_id,
is_profile_view: Some(true),
page,
limit,
creator_id,
..Default::default()
}
.list(&mut context.pool())
.await?;
let comments = CommentQuery::builder()
.pool(&mut context.pool())
.local_user(local_user_clone.as_ref())
.sort(sort.map(post_to_comment_sort_type))
.saved_only(saved_only)
.show_deleted_and_removed(Some(false))
.community_id(community_id)
.page(page)
.limit(limit)
.creator_id(
// If its saved only, you don't care what creator it was
// Or, if its not saved, then you only want it for that specific creator
if !saved_only.unwrap_or(false) {
Some(person_details_id)
} else {
None
},
)
.build()
.list()
.await?;
let comments = CommentQuery {
local_user: (local_user_view.as_ref()),
sort: (sort.map(post_to_comment_sort_type)),
saved_only: (saved_only),
show_deleted_and_removed: (Some(false)),
community_id: (community_id),
is_profile_view: Some(true),
page: (page),
limit: (limit),
creator_id,
..Default::default()
}
.list(&mut context.pool())
.await?;
let moderates =
CommunityModeratorView::for_person(&mut context.pool(), person_details_id).await?;

View file

@ -50,119 +50,116 @@ pub async fn search(
data.community_id
};
let creator_id = data.creator_id;
let local_user = local_user_view.map(|l| l.local_user);
let local_user = local_user_view.as_ref().map(|l| l.local_user.clone());
match search_type {
SearchType::Posts => {
posts = PostQuery::builder()
.pool(&mut context.pool())
.sort(sort)
.listing_type(listing_type)
.community_id(community_id)
.creator_id(creator_id)
.local_user(local_user.as_ref())
.search_term(Some(q))
.is_mod_or_admin(is_admin)
.page(page)
.limit(limit)
.build()
.list()
.await?;
posts = PostQuery {
sort: (sort),
listing_type: (listing_type),
community_id: (community_id),
creator_id: (creator_id),
local_user: (local_user_view.as_ref()),
search_term: (Some(q)),
page: (page),
limit: (limit),
..Default::default()
}
.list(&mut context.pool())
.await?;
}
SearchType::Comments => {
comments = CommentQuery::builder()
.pool(&mut context.pool())
.sort(sort.map(post_to_comment_sort_type))
.listing_type(listing_type)
.search_term(Some(q))
.community_id(community_id)
.creator_id(creator_id)
.local_user(local_user.as_ref())
.page(page)
.limit(limit)
.build()
.list()
.await?;
comments = CommentQuery {
sort: (sort.map(post_to_comment_sort_type)),
listing_type: (listing_type),
search_term: (Some(q)),
community_id: (community_id),
creator_id: (creator_id),
local_user: (local_user_view.as_ref()),
page: (page),
limit: (limit),
..Default::default()
}
.list(&mut context.pool())
.await?;
}
SearchType::Communities => {
communities = CommunityQuery::builder()
.pool(&mut context.pool())
.sort(sort)
.listing_type(listing_type)
.search_term(Some(q))
.local_user(local_user.as_ref())
.is_mod_or_admin(is_admin)
.page(page)
.limit(limit)
.build()
.list()
.await?;
communities = CommunityQuery {
sort: (sort),
listing_type: (listing_type),
search_term: (Some(q)),
local_user: (local_user.as_ref()),
is_mod_or_admin: (is_admin),
page: (page),
limit: (limit),
..Default::default()
}
.list(&mut context.pool())
.await?;
}
SearchType::Users => {
users = PersonQuery::builder()
.pool(&mut context.pool())
.sort(sort)
.search_term(Some(q))
.page(page)
.limit(limit)
.build()
.list()
.await?;
users = PersonQuery {
sort: (sort),
search_term: (Some(q)),
page: (page),
limit: (limit),
}
.list(&mut context.pool())
.await?;
}
SearchType::All => {
// If the community or creator is included, dont search communities or users
let community_or_creator_included =
data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some();
let local_user_ = local_user.clone();
posts = PostQuery::builder()
.pool(&mut context.pool())
.sort(sort)
.listing_type(listing_type)
.community_id(community_id)
.creator_id(creator_id)
.local_user(local_user_.as_ref())
.search_term(Some(q))
.is_mod_or_admin(is_admin)
.page(page)
.limit(limit)
.build()
.list()
.await?;
let q = data.q.clone();
posts = PostQuery {
sort: (sort),
listing_type: (listing_type),
community_id: (community_id),
creator_id: (creator_id),
local_user: (local_user_view.as_ref()),
search_term: (Some(q)),
page: (page),
limit: (limit),
..Default::default()
}
.list(&mut context.pool())
.await?;
let q = data.q.clone();
let local_user_ = local_user.clone();
comments = CommentQuery::builder()
.pool(&mut context.pool())
.sort(sort.map(post_to_comment_sort_type))
.listing_type(listing_type)
.search_term(Some(q))
.community_id(community_id)
.creator_id(creator_id)
.local_user(local_user_.as_ref())
.page(page)
.limit(limit)
.build()
.list()
.await?;
comments = CommentQuery {
sort: (sort.map(post_to_comment_sort_type)),
listing_type: (listing_type),
search_term: (Some(q)),
community_id: (community_id),
creator_id: (creator_id),
local_user: (local_user_view.as_ref()),
page: (page),
limit: (limit),
..Default::default()
}
.list(&mut context.pool())
.await?;
let q = data.q.clone();
communities = if community_or_creator_included {
vec![]
} else {
CommunityQuery::builder()
.pool(&mut context.pool())
.sort(sort)
.listing_type(listing_type)
.search_term(Some(q))
.local_user(local_user.as_ref())
.is_mod_or_admin(is_admin)
.page(page)
.limit(limit)
.build()
.list()
.await?
CommunityQuery {
sort: (sort),
listing_type: (listing_type),
search_term: (Some(q)),
local_user: (local_user.as_ref()),
is_mod_or_admin: (is_admin),
page: (page),
limit: (limit),
..Default::default()
}
.list(&mut context.pool())
.await?
};
let q = data.q.clone();
@ -170,31 +167,29 @@ pub async fn search(
users = if community_or_creator_included {
vec![]
} else {
PersonQuery::builder()
.pool(&mut context.pool())
.sort(sort)
.search_term(Some(q))
.page(page)
.limit(limit)
.build()
.list()
.await?
PersonQuery {
sort: (sort),
search_term: (Some(q)),
page: (page),
limit: (limit),
}
.list(&mut context.pool())
.await?
};
}
SearchType::Url => {
posts = PostQuery::builder()
.pool(&mut context.pool())
.sort(sort)
.listing_type(listing_type)
.community_id(community_id)
.creator_id(creator_id)
.url_search(Some(q))
.is_mod_or_admin(is_admin)
.page(page)
.limit(limit)
.build()
.list()
.await?;
posts = PostQuery {
sort: (sort),
listing_type: (listing_type),
community_id: (community_id),
creator_id: (creator_id),
url_search: (Some(q)),
page: (page),
limit: (limit),
..Default::default()
}
.list(&mut context.pool())
.await?;
}
};

View file

@ -78,18 +78,20 @@ impl Collection for ApubCommunityModerators {
// Add new mods to database which have been added to moderators collection
for mod_id in apub.ordered_items {
let mod_user: ApubPerson = mod_id.dereference(data).await?;
if !current_moderators
.iter()
.map(|c| c.moderator.actor_id.clone())
.any(|x| x == mod_user.actor_id)
{
let community_moderator_form = CommunityModeratorForm {
community_id: owner.id,
person_id: mod_user.id,
};
CommunityModerator::join(&mut data.pool(), &community_moderator_form).await?;
// Ignore errors as mod accounts might be deleted or instances unavailable.
let mod_user: Option<ApubPerson> = mod_id.dereference(data).await.ok();
if let Some(mod_user) = mod_user {
if !current_moderators
.iter()
.map(|c| c.moderator.actor_id.clone())
.any(|x| x == mod_user.actor_id)
{
let community_moderator_form = CommunityModeratorForm {
community_id: owner.id,
person_id: mod_user.id,
};
CommunityModerator::join(&mut data.pool(), &community_moderator_form).await?;
}
}
}
@ -100,6 +102,9 @@ impl Collection for ApubCommunityModerators {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
objects::{

View file

@ -13,7 +13,7 @@ use activitypub_federation::{
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use http::StatusCode;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::activity::Activity;
use lemmy_db_schema::source::activity::SentActivity;
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
use serde::{Deserialize, Serialize};
use std::ops::Deref;
@ -88,12 +88,10 @@ pub(crate) async fn get_activity(
info.id
))?
.into();
let activity = Activity::read_from_apub_id(&mut context.pool(), &activity_id).await?;
let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id).await?;
let sensitive = activity.sensitive;
if !activity.local {
Err(err_object_not_local())
} else if sensitive {
if sensitive {
Ok(HttpResponse::Forbidden().finish())
} else {
create_apub_response(&activity.data)

View file

@ -3,18 +3,12 @@ use activitypub_federation::config::{Data, UrlVerifier};
use async_trait::async_trait;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
activity::{Activity, ActivityInsertForm},
instance::Instance,
local_site::LocalSite,
},
traits::Crud,
source::{activity::ReceivedActivity, instance::Instance, local_site::LocalSite},
utils::{ActualDbPool, DbPool},
};
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
use moka::future::Cache;
use once_cell::sync::Lazy;
use serde::Serialize;
use std::{sync::Arc, time::Duration};
use url::Url;
@ -178,30 +172,16 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
Ok(())
}
/// Store a sent or received activity in the database.
/// Store received activities in the database.
///
/// Stored activities are served over the HTTP endpoint `GET /activities/{type_}/{id}`. This also
/// ensures that the same activity cannot be received more than once.
#[tracing::instrument(skip(data, activity))]
async fn insert_activity<T>(
/// This ensures that the same activity doesnt get received and processed more than once, which
/// would be a waste of resources.
#[tracing::instrument(skip(data))]
async fn insert_received_activity(
ap_id: &Url,
activity: &T,
local: bool,
sensitive: bool,
data: &Data<LemmyContext>,
) -> Result<(), LemmyError>
where
T: Serialize,
{
let ap_id = ap_id.clone().into();
let form = ActivityInsertForm {
ap_id,
data: serde_json::to_value(activity)?,
local: Some(local),
sensitive: Some(sensitive),
updated: None,
};
Activity::create(&mut data.pool(), &form).await?;
) -> Result<(), LemmyError> {
ReceivedActivity::create(&mut data.pool(), &ap_id.clone().into()).await?;
Ok(())
}

View file

@ -179,6 +179,9 @@ impl Object for ApubComment {
#[cfg(test)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
objects::{

View file

@ -204,6 +204,9 @@ impl ApubCommunity {
#[cfg(test)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
objects::{instance::tests::parse_lemmy_instance, tests::init_context},

View file

@ -206,6 +206,9 @@ pub(crate) async fn remote_instance_inboxes(pool: &mut DbPool<'_>) -> Result<Vec
#[cfg(test)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{objects::tests::init_context, protocol::tests::file_to_json_object};
use lemmy_db_schema::traits::Crud;

View file

@ -54,6 +54,9 @@ pub(crate) fn verify_is_remote_object(id: &Url, settings: &Settings) -> Result<(
#[cfg(test)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use activitypub_federation::config::{Data, FederationConfig};
use anyhow::anyhow;
use lemmy_api_common::{context::LemmyContext, request::build_user_agent};

View file

@ -195,6 +195,9 @@ impl Actor for ApubPerson {
#[cfg(test)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
objects::{

View file

@ -280,6 +280,9 @@ impl Object for ApubPost {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
objects::{

View file

@ -136,6 +136,9 @@ impl Object for ApubPrivateMessage {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
objects::{

View file

@ -3,6 +3,9 @@ pub mod undo_block_user;
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
tests::test_parse_lemmy_item,

View file

@ -7,6 +7,9 @@ pub mod update;
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
activities::community::{
announce::AnnounceActivity,

View file

@ -4,6 +4,9 @@ pub mod page;
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
activities::create_or_update::{
chat_message::CreateOrUpdateChatMessage,

View file

@ -4,6 +4,9 @@ pub mod undo_delete;
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
activities::deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete},
tests::test_parse_lemmy_item,

View file

@ -4,6 +4,9 @@ pub mod undo_follow;
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
activities::following::{accept::AcceptFollow, follow::Follow, undo_follow::UndoFollow},
tests::test_parse_lemmy_item,

View file

@ -16,6 +16,9 @@ pub enum CreateOrUpdateType {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
activities::{
community::announce::AnnounceActivity,

View file

@ -3,6 +3,9 @@ pub mod vote;
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
activities::voting::{undo_vote::UndoVote, vote::Vote},
tests::test_parse_lemmy_item,

View file

@ -6,6 +6,9 @@ pub(crate) mod group_outbox;
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
collections::{
empty_outbox::EmptyOutbox,

View file

@ -89,6 +89,9 @@ pub trait InCommunity {
#[cfg(test)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use activitypub_federation::protocol::context::WithContext;
use assert_json_diff::assert_json_include;
use lemmy_utils::error::LemmyError;

View file

@ -95,6 +95,9 @@ impl LanguageTag {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{
objects::{
chat_message::ChatMessage,

View file

@ -242,6 +242,9 @@ where
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::protocol::{objects::page::Page, tests::test_parse_lemmy_item};
#[test]

View file

@ -14,9 +14,27 @@ path = "src/lib.rs"
doctest = false
[features]
full = ["diesel", "diesel-derive-newtype", "diesel-derive-enum", "diesel_migrations", "bcrypt", "lemmy_utils",
"activitypub_federation", "sha2", "regex", "once_cell", "serde_json", "diesel_ltree",
"diesel-async", "deadpool", "ts-rs"]
full = [
"diesel",
"diesel-derive-newtype",
"diesel-derive-enum",
"diesel_migrations",
"bcrypt",
"lemmy_utils",
"activitypub_federation",
"sha2",
"regex",
"once_cell",
"serde_json",
"diesel_ltree",
"diesel-async",
"deadpool",
"ts-rs",
"tokio",
"tokio-postgres",
"tokio-postgres-rustls",
"rustls",
]
[dependencies]
chrono = { workspace = true }
@ -29,25 +47,33 @@ serde_json = { workspace = true, optional = true }
activitypub_federation = { workspace = true, optional = true }
lemmy_utils = { workspace = true, optional = true }
bcrypt = { workspace = true, optional = true }
diesel = { workspace = true, features = ["postgres","chrono", "serde_json", "uuid"], optional = true }
diesel = { workspace = true, features = [
"postgres",
"chrono",
"serde_json",
"uuid",
], optional = true }
diesel-derive-newtype = { workspace = true, optional = true }
diesel-derive-enum = { workspace = true, optional = true }
diesel_migrations = { workspace = true, optional = true }
diesel-async = { workspace = true, features = ["postgres", "deadpool"], optional = true }
diesel-async = { workspace = true, features = [
"postgres",
"deadpool",
], optional = true }
sha2 = { workspace = true, optional = true }
regex = { workspace = true, optional = true }
once_cell = { workspace = true, optional = true }
diesel_ltree = { workspace = true, optional = true }
typed-builder = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
deadpool = { version = "0.9.5", features = ["rt_tokio_1"], optional = true }
ts-rs = { workspace = true, optional = true }
rustls = { workspace = true }
futures-util = { workspace = true }
tokio-postgres = { workspace = true }
tokio-postgres-rustls = { workspace = true }
tokio = { workspace = true, optional = true }
tokio-postgres = { workspace = true, optional = true }
tokio-postgres-rustls = { workspace = true, optional = true }
rustls = { workspace = true, optional = true }
uuid = { workspace = true, features = ["v4"] }
[dev-dependencies]

View file

@ -35,6 +35,9 @@ impl CommentAggregates {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::comment_aggregates::CommentAggregates,
source::{

View file

@ -19,6 +19,9 @@ impl CommunityAggregates {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::community_aggregates::CommunityAggregates,
source::{

View file

@ -19,6 +19,9 @@ impl PersonAggregates {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::person_aggregates::PersonAggregates,
source::{

View file

@ -35,6 +35,9 @@ impl PostAggregates {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::post_aggregates::PostAggregates,
source::{

View file

@ -15,6 +15,9 @@ impl SiteAggregates {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::site_aggregates::SiteAggregates,
source::{

View file

@ -96,6 +96,8 @@ pub struct PostAggregates {
pub featured_local: bool,
pub hot_rank: i32,
pub hot_rank_active: i32,
pub community_id: CommunityId,
pub creator_id: PersonId,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]

View file

@ -1,139 +1,115 @@
use crate::{
diesel::OptionalExtension,
newtypes::DbUrl,
schema::activity::dsl::{activity, ap_id},
source::activity::{Activity, ActivityInsertForm, ActivityUpdateForm},
traits::Crud,
source::activity::{ReceivedActivity, SentActivity, SentActivityForm},
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel::{
dsl::insert_into,
result::{DatabaseErrorKind, Error, Error::DatabaseError},
ExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;
#[async_trait]
impl Crud for Activity {
type InsertForm = ActivityInsertForm;
type UpdateForm = ActivityUpdateForm;
type IdType = i32;
async fn create(pool: &mut DbPool<'_>, new_activity: &Self::InsertForm) -> Result<Self, Error> {
pub async fn create(pool: &mut DbPool<'_>, form: SentActivityForm) -> Result<Self, Error> {
use crate::schema::sent_activity::dsl::sent_activity;
let conn = &mut get_conn(pool).await?;
insert_into(activity)
.values(new_activity)
insert_into(sent_activity)
.values(form)
.get_result::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
activity_id: i32,
new_activity: &Self::UpdateForm,
) -> Result<Self, Error> {
pub async fn read_from_apub_id(pool: &mut DbPool<'_>, object_id: &DbUrl) -> Result<Self, Error> {
use crate::schema::sent_activity::dsl::{ap_id, sent_activity};
let conn = &mut get_conn(pool).await?;
diesel::update(activity.find(activity_id))
.set(new_activity)
.get_result::<Self>(conn)
.await
}
async fn delete(pool: &mut DbPool<'_>, activity_id: i32) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
diesel::delete(activity.find(activity_id))
.execute(conn)
.await
}
}
impl Activity {
pub async fn read_from_apub_id(
pool: &mut DbPool<'_>,
object_id: &DbUrl,
) -> Result<Activity, Error> {
let conn = &mut get_conn(pool).await?;
activity
sent_activity
.filter(ap_id.eq(object_id))
.first::<Self>(conn)
.await
}
}
impl ReceivedActivity {
pub async fn create(pool: &mut DbPool<'_>, ap_id_: &DbUrl) -> Result<(), Error> {
use crate::schema::received_activity::dsl::{ap_id, id, received_activity};
let conn = &mut get_conn(pool).await?;
let res = insert_into(received_activity)
.values(ap_id.eq(ap_id_))
.on_conflict_do_nothing()
.returning(id)
.get_result::<i64>(conn)
.await
.optional()?;
if res.is_some() {
// new activity inserted successfully
Ok(())
} else {
// duplicate activity
Err(DatabaseError(
DatabaseErrorKind::UniqueViolation,
Box::<String>::default(),
))
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
newtypes::DbUrl,
source::{
activity::{Activity, ActivityInsertForm},
instance::Instance,
person::{Person, PersonInsertForm},
},
utils::build_db_pool_for_tests,
};
use serde_json::Value;
use crate::utils::build_db_pool_for_tests;
use serde_json::json;
use serial_test::serial;
use url::Url;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn receive_activity_duplicate() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let ap_id: DbUrl = Url::parse("http://example.com/activity/531")
.unwrap()
.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
// inserting activity for first time
let res = ReceivedActivity::create(pool, &ap_id).await;
assert!(res.is_ok());
let creator_form = PersonInsertForm::builder()
.name("activity_creator_ pm".into())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let res = ReceivedActivity::create(pool, &ap_id).await;
assert!(res.is_err());
}
let inserted_creator = Person::create(pool, &creator_form).await.unwrap();
#[tokio::test]
#[serial]
async fn sent_activity_write_read() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let ap_id: DbUrl = Url::parse("http://example.com/activity/412")
.unwrap()
.into();
let data = json!({
"key1": "0xF9BA143B95FF6D82",
"key2": "42",
});
let sensitive = false;
let ap_id_: DbUrl = Url::parse(
"https://enterprise.lemmy.ml/activities/delete/f1b5d57c-80f8-4e03-a615-688d552e946c",
)
.unwrap()
.into();
let test_json: Value = serde_json::from_str(
r#"{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/activities/delete/f1b5d57c-80f8-4e03-a615-688d552e946c",
"type": "Delete",
"actor": "https://enterprise.lemmy.ml/u/riker",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"https://enterprise.lemmy.ml/c/main/"
],
"object": "https://enterprise.lemmy.ml/post/32"
}"#,
)
.unwrap();
let activity_form = ActivityInsertForm {
ap_id: ap_id_.clone(),
data: test_json.clone(),
local: Some(true),
sensitive: Some(false),
updated: None,
let form = SentActivityForm {
ap_id: ap_id.clone(),
data: data.clone(),
sensitive,
};
let inserted_activity = Activity::create(pool, &activity_form).await.unwrap();
SentActivity::create(pool, form).await.unwrap();
let expected_activity = Activity {
ap_id: ap_id_.clone(),
id: inserted_activity.id,
data: test_json,
local: true,
sensitive: false,
published: inserted_activity.published,
updated: None,
};
let read_activity = Activity::read(pool, inserted_activity.id).await.unwrap();
let read_activity_by_apub_id = Activity::read_from_apub_id(pool, &ap_id_).await.unwrap();
Person::delete(pool, inserted_creator.id).await.unwrap();
Activity::delete(pool, inserted_activity.id).await.unwrap();
assert_eq!(expected_activity, read_activity);
assert_eq!(expected_activity, read_activity_by_apub_id);
assert_eq!(expected_activity, inserted_activity);
let res = SentActivity::read_from_apub_id(pool, &ap_id).await.unwrap();
assert_eq!(res.ap_id, ap_id);
assert_eq!(res.data, data);
assert_eq!(res.sensitive, sensitive);
}
}

View file

@ -275,25 +275,37 @@ impl CommunityLanguage {
return Ok(());
}
let form = lang_ids
.into_iter()
.map(|language_id| CommunityLanguageForm {
community_id: for_community_id,
language_id,
})
.collect::<Vec<_>>();
conn
.build_transaction()
.run(|conn| {
Box::pin(async move {
use crate::schema::community_language::dsl::{community_id, community_language};
use diesel::result::DatabaseErrorKind::UniqueViolation;
// Clear the current languages
delete(community_language.filter(community_id.eq(for_community_id)))
.execute(conn)
.await?;
for l in lang_ids {
let form = CommunityLanguageForm {
community_id: for_community_id,
language_id: l,
};
insert_into(community_language)
.values(form)
.get_result::<Self>(conn)
.await?;
let insert_res = insert_into(community_language)
.values(form)
.get_result::<Self>(conn)
.await;
if let Err(Error::DatabaseError(UniqueViolation, _info)) = insert_res {
// race condition: this function was probably called simultaneously from another caller. ignore error
// tracing::warn!("unique error: {_info:#?}");
// _info.constraint_name() should be = "community_language_community_id_language_id_key"
return Ok(());
} else {
insert_res?;
}
Ok(())
}) as _
@ -372,6 +384,9 @@ async fn convert_read_languages(
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
impls::actor_language::{

View file

@ -50,6 +50,9 @@ impl CaptchaAnswer {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::captcha_answer::{CaptchaAnswer, CaptchaAnswerForm, CheckCaptchaAnswer},
utils::build_db_pool_for_tests,

View file

@ -247,6 +247,9 @@ impl Saveable for CommentSaved {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
newtypes::LanguageId,
source::{

View file

@ -74,6 +74,9 @@ impl CommentReply {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{
comment::{Comment, CommentInsertForm},

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