Merge upstream/main into migration-runner (using imerge)

This commit is contained in:
Dull Bananas 2024-11-12 00:04:30 -07:00
commit f636f1a574
383 changed files with 13411 additions and 10407 deletions

View file

@ -2,7 +2,7 @@
# See https://github.com/woodpecker-ci/woodpecker/issues/1677 # See https://github.com/woodpecker-ci/woodpecker/issues/1677
variables: variables:
- &rust_image "rust:1.80" - &rust_image "rust:1.81"
- &rust_nightly_image "rustlang/rust:nightly" - &rust_nightly_image "rustlang/rust:nightly"
- &install_pnpm "corepack enable pnpm" - &install_pnpm "corepack enable pnpm"
- &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin" - &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin"
@ -42,14 +42,14 @@ steps:
- event: [pull_request, tag] - event: [pull_request, tag]
prettier_check: prettier_check:
image: tmknom/prettier:3.0.0 image: tmknom/prettier:3.2.5
commands: commands:
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml' - prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml'
when: when:
- event: pull_request - event: pull_request
toml_fmt: toml_fmt:
image: tamasfe/taplo:0.8.1 image: tamasfe/taplo:0.9.3
commands: commands:
- taplo format --check - taplo format --check
when: when:
@ -73,12 +73,12 @@ steps:
when: when:
- event: pull_request - event: pull_request
cargo_machete: cargo_shear:
image: *rust_nightly_image image: *rust_nightly_image
commands: commands:
- *install_binstall - *install_binstall
- cargo binstall -y cargo-machete - cargo binstall -y cargo-shear
- cargo machete - cargo shear
when: when:
- event: pull_request - event: pull_request
@ -122,7 +122,6 @@ steps:
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
commands: commands:
- export LEMMY_CONFIG_LOCATION=./config/config.hjson
- ./scripts/update_config_defaults.sh config/defaults_current.hjson - ./scripts/update_config_defaults.sh config/defaults_current.hjson
- diff config/defaults.hjson config/defaults_current.hjson - diff config/defaults.hjson config/defaults_current.hjson
when: *slow_check_paths when: *slow_check_paths
@ -147,7 +146,6 @@ steps:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
commands: commands:
# same as scripts/db_perf.sh but without creating a new database server # same as scripts/db_perf.sh but without creating a new database server
- export LEMMY_CONFIG_LOCATION=config/config.hjson
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1 - cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
when: *slow_check_paths when: *slow_check_paths
@ -157,7 +155,7 @@ steps:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
commands: commands:
- rustup component add clippy - rustup component add clippy
- cargo clippy --workspace --tests --all-targets --features console -- -D warnings - cargo clippy --workspace --tests --all-targets -- -D warnings
when: *slow_check_paths when: *slow_check_paths
cargo_build: cargo_build:
@ -177,9 +175,10 @@ steps:
RUST_BACKTRACE: "1" RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
commands: commands:
- cp crates/db_schema/src/schema.rs tmp.schema
- target/lemmy_server migration --all run - target/lemmy_server migration --all run
- <<: *install_diesel_cli - <<: *install_diesel_cli
- diesel print-schema --config-file=diesel.toml > tmp.schema - diesel print-schema
- diff tmp.schema crates/db_schema/src/schema.rs - diff tmp.schema crates/db_schema/src/schema.rs
when: *slow_check_paths when: *slow_check_paths
@ -190,6 +189,7 @@ steps:
RUST_BACKTRACE: "1" RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
LEMMY_TEST_FAST_FEDERATION: "1" LEMMY_TEST_FAST_FEDERATION: "1"
LEMMY_CONFIG_LOCATION: ../../config/config.hjson
commands: commands:
# Install pg_dump for the schema setup test (must match server version) # Install pg_dump for the schema setup test (must match server version)
- apt update && apt install -y lsb-release - apt update && apt install -y lsb-release
@ -197,12 +197,20 @@ steps:
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
- apt update && apt install -y postgresql-client-16 - apt update && apt install -y postgresql-client-16
# Run tests # Run tests
- export LEMMY_CONFIG_LOCATION=../../config/config.hjson
- cargo test --workspace --no-fail-fast - cargo test --workspace --no-fail-fast
when: *slow_check_paths when: *slow_check_paths
check_ts_bindings:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- ./scripts/ts_bindings_check.sh
when:
- event: pull_request
run_federation_tests: run_federation_tests:
image: node:20-bookworm-slim image: node:22-bookworm-slim
environment: environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432 LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
DO_WRITE_HOSTS_FILE: "1" DO_WRITE_HOSTS_FILE: "1"
@ -227,10 +235,13 @@ steps:
publish_release_docker: publish_release_docker:
image: woodpeckerci/plugin-docker-buildx image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
settings: settings:
repo: dessalines/lemmy repo: dessalines/lemmy
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
username:
from_secret: docker_username
password:
from_secret: docker_password
platforms: linux/amd64, linux/arm64 platforms: linux/amd64, linux/arm64
build_args: build_args:
- RUST_RELEASE_MODE=release - RUST_RELEASE_MODE=release
@ -240,10 +251,13 @@ steps:
nightly_build: nightly_build:
image: woodpeckerci/plugin-docker-buildx image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
settings: settings:
repo: dessalines/lemmy repo: dessalines/lemmy
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
username:
from_secret: docker_username
password:
from_secret: docker_password
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build_args: build_args:
- RUST_RELEASE_MODE=release - RUST_RELEASE_MODE=release

2836
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
[workspace.package] [workspace.package]
version = "0.19.6-beta.6" version = "0.19.6-beta.7"
edition = "2021" edition = "2021"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -24,11 +24,12 @@ doctest = false
[lints] [lints]
workspace = true workspace = true
# See https://github.com/johnthagen/min-sized-rust for additional optimizations
[profile.release] [profile.release]
debug = 0 debug = 0
lto = "thin" lto = "fat"
strip = true # Automatically strip symbols from the binary. opt-level = 3 # Optimize for speed, not size.
opt-level = "z" # Optimize for size. codegen-units = 1 # Reduce parallel code generation.
# This profile significantly speeds up build time. If debug info is needed you can comment the line # 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. # out temporarily, but make sure to leave this in the main branch.
@ -36,16 +37,6 @@ opt-level = "z" # Optimize for size.
debug = 0 debug = 0
[features] [features]
embed-pictrs = ["pict-rs"]
# This feature requires building with `tokio_unstable` flag, see documentation:
# https://docs.rs/tokio/latest/tokio/#unstable-features
console = [
"console-subscriber",
"opentelemetry",
"opentelemetry-otlp",
"tracing-opentelemetry",
"reqwest-tracing/opentelemetry_0_16",
]
json-log = ["tracing-subscriber/json"] json-log = ["tracing-subscriber/json"]
default = [] default = []
@ -87,20 +78,21 @@ uninlined_format_args = "allow"
unused_self = "deny" unused_self = "deny"
unwrap_used = "deny" unwrap_used = "deny"
unimplemented = "deny" unimplemented = "deny"
unused_async = "deny"
[workspace.dependencies] [workspace.dependencies]
lemmy_api = { version = "=0.19.6-beta.6", path = "./crates/api" } lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.6-beta.6", path = "./crates/api_crud" } lemmy_api_crud = { version = "=0.19.6-beta.7", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.6-beta.6", path = "./crates/apub" } lemmy_apub = { version = "=0.19.6-beta.7", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.6-beta.6", path = "./crates/utils", default-features = false } lemmy_utils = { version = "=0.19.6-beta.7", path = "./crates/utils", default-features = false }
lemmy_db_schema = { version = "=0.19.6-beta.6", path = "./crates/db_schema" } lemmy_db_schema = { version = "=0.19.6-beta.7", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.6-beta.6", path = "./crates/api_common" } lemmy_api_common = { version = "=0.19.6-beta.7", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.6-beta.6", path = "./crates/routes" } lemmy_routes = { version = "=0.19.6-beta.7", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.6-beta.6", path = "./crates/db_views" } lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.6-beta.6", path = "./crates/db_views_actor" } lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.6-beta.6", path = "./crates/db_views_moderator" } lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" }
lemmy_federate = { version = "=0.19.6-beta.6", path = "./crates/federate" } lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" }
activitypub_federation = { version = "0.5.8", default-features = false, features = [ activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [
"actix-web", "actix-web",
] } ] }
diesel = "2.1.6" diesel = "2.1.6"
@ -108,7 +100,7 @@ diesel_migrations = "2.1.0"
diesel-async = "0.4.1" diesel-async = "0.4.1"
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_with = "3.9.0" serde_with = "3.9.0"
actix-web = { version = "4.8.0", default-features = false, features = [ actix-web = { version = "4.9.0", default-features = false, features = [
"macros", "macros",
"rustls-0_23", "rustls-0_23",
"compress-brotli", "compress-brotli",
@ -117,19 +109,17 @@ actix-web = { version = "4.8.0", default-features = false, features = [
"cookies", "cookies",
] } ] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-actix-web = { version = "0.7.11", default-features = false } tracing-actix-web = { version = "0.7.10", default-features = false }
tracing-error = "0.2.0"
tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.2", features = ["serde"] } url = { version = "2.5.2", features = ["serde"] }
reqwest = { version = "0.11.27", default-features = false, features = [ reqwest = { version = "0.12.7", default-features = false, features = [
"json", "json",
"blocking", "blocking",
"gzip", "gzip",
"rustls-tls", "rustls-tls",
] } ] }
reqwest-middleware = "0.2.5" reqwest-middleware = "0.3.3"
reqwest-tracing = "0.4.8" reqwest-tracing = "0.5.3"
clokwerk = "0.4.0" clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] } doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.1" bcrypt = "0.15.1"
@ -143,7 +133,6 @@ anyhow = { version = "1.0.86", features = [
"backtrace", "backtrace",
] } # backtrace is on by default on nightly, but not stable rust ] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.1" diesel_ltree = "0.3.1"
typed-builder = "0.19.1"
serial_test = "3.1.1" serial_test = "3.1.1"
tokio = { version = "1.39.2", features = ["full"] } tokio = { version = "1.39.2", features = ["full"] }
regex = "1.10.5" regex = "1.10.5"
@ -152,14 +141,13 @@ diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] }
itertools = "0.13.0" itertools = "0.13.0"
futures = "0.3.30" futures = "0.3.30"
http = "0.2.12" http = "1.1"
rosetta-i18n = "0.1.3" rosetta-i18n = "0.1.3"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] } ts-rs = { version = "10.0.0", features = [
tracing-opentelemetry = { version = "0.19.0" }
ts-rs = { version = "7.1.1", features = [
"serde-compat", "serde-compat",
"chrono-impl", "chrono-impl",
"no-serde-warnings", "no-serde-warnings",
"url-impl",
] } ] }
rustls = { version = "0.23.12", features = ["ring"] } rustls = { version = "0.23.12", features = ["ring"] }
futures-util = "0.3.30" futures-util = "0.3.30"
@ -171,7 +159,9 @@ moka = { version = "0.12.8", features = ["future"] }
i-love-jesus = { version = "0.1.0" } i-love-jesus = { version = "0.1.0" }
clap = { version = "4.5.13", features = ["derive", "env"] } clap = { version = "4.5.13", features = ["derive", "env"] }
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
derive-new = "0.6.0" derive-new = "0.7.0"
diesel-bind-if-some = "0.1.0"
tuplex = "0.1.2"
[dependencies] [dependencies]
lemmy_api = { workspace = true } lemmy_api = { workspace = true }
@ -188,8 +178,6 @@ diesel-async = { workspace = true }
actix-web = { workspace = true } actix-web = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-actix-web = { workspace = true } tracing-actix-web = { workspace = true }
tracing-error = { workspace = true }
tracing-log = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
url = { workspace = true } url = { workspace = true }
reqwest = { workspace = true } reqwest = { workspace = true }
@ -197,11 +185,6 @@ reqwest-middleware = { workspace = true }
reqwest-tracing = { workspace = true } reqwest-tracing = { workspace = true }
clokwerk = { workspace = true } clokwerk = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry = { workspace = true, optional = true }
console-subscriber = { version = "0.4.0", optional = true }
opentelemetry-otlp = { version = "0.12.0", optional = true }
pict-rs = { version = "0.5.16", optional = true }
rustls = { workspace = true } rustls = { workspace = true }
tokio.workspace = true tokio.workspace = true
actix-cors = "0.7.0" actix-cors = "0.7.0"
@ -210,7 +193,7 @@ chrono = { workspace = true }
prometheus = { version = "0.13.4", features = ["process"] } prometheus = { version = "0.13.4", features = ["process"] }
serial_test = { workspace = true } serial_test = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
actix-web-prom = "0.8.0" actix-web-prom = "0.9.0"
[dev-dependencies] [dev-dependencies]
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }

View file

@ -6,31 +6,32 @@
"repository": "https://github.com/LemmyNet/lemmy", "repository": "https://github.com/LemmyNet/lemmy",
"author": "Dessalines", "author": "Dessalines",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"packageManager": "pnpm@9.6.0", "packageManager": "pnpm@9.12.3",
"scripts": { "scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'", "lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'",
"fix": "prettier --write src && eslint --fix src", "fix": "prettier --write src && eslint --fix src",
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ", "api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i private_community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ",
"api-test-follow": "jest -i follow.spec.ts", "api-test-follow": "jest -i follow.spec.ts",
"api-test-comment": "jest -i comment.spec.ts", "api-test-comment": "jest -i comment.spec.ts",
"api-test-post": "jest -i post.spec.ts", "api-test-post": "jest -i post.spec.ts",
"api-test-user": "jest -i user.spec.ts", "api-test-user": "jest -i user.spec.ts",
"api-test-community": "jest -i community.spec.ts", "api-test-community": "jest -i community.spec.ts",
"api-test-private-community": "jest -i private_community.spec.ts",
"api-test-private-message": "jest -i private_message.spec.ts", "api-test-private-message": "jest -i private_message.spec.ts",
"api-test-image": "jest -i image.spec.ts" "api-test-image": "jest -i image.spec.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^22.0.2", "@types/node": "^22.3.0",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.1.0",
"eslint": "^9.8.0", "eslint": "^9.9.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.19.5-alpha.1", "lemmy-js-client": "0.20.0-private-community.9",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"typescript-eslint": "^8.0.0" "typescript-eslint": "^8.1.0"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -92,7 +92,7 @@ test("Create a comment", async () => {
test("Create a comment in a non-existent post", async () => { test("Create a comment in a non-existent post", async () => {
await expect(createComment(alpha, -1)).rejects.toStrictEqual( await expect(createComment(alpha, -1)).rejects.toStrictEqual(
Error("couldnt_find_post"), Error("not_found"),
); );
}); });
@ -143,7 +143,7 @@ test("Delete a comment", async () => {
await waitUntil( await waitUntil(
() => () =>
resolveComment(gamma, commentRes.comment_view.comment).catch(e => e), resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
r => r.message !== "couldnt_find_object", r => r.message !== "not_found",
) )
).comment; ).comment;
if (!gammaComment) { if (!gammaComment) {
@ -158,16 +158,16 @@ test("Delete a comment", async () => {
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
expect(deleteCommentRes.comment_view.comment.content).toBe(""); expect(deleteCommentRes.comment_view.comment.content).toBe("");
// Make sure that comment is undefined on beta // Make sure that comment is deleted on beta
await waitUntil( await waitUntil(
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), () => resolveComment(beta, commentRes.comment_view.comment),
e => e.message == "couldnt_find_object", c => c.comment?.comment.deleted === true,
); );
// Make sure that comment is undefined on gamma after delete // Make sure that comment is deleted on gamma after delete
await waitUntil( await waitUntil(
() => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e), () => resolveComment(gamma, commentRes.comment_view.comment),
e => e.message === "couldnt_find_object", c => c.comment?.comment.deleted === true,
); );
// Test undeleting the comment // Test undeleting the comment
@ -181,11 +181,10 @@ test("Delete a comment", async () => {
// Make sure that comment is undeleted on beta // Make sure that comment is undeleted on beta
let betaComment2 = ( let betaComment2 = (
await waitUntil( await waitUntil(
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), () => resolveComment(beta, commentRes.comment_view.comment),
e => e.message !== "couldnt_find_object", c => c.comment?.comment.deleted === false,
) )
).comment; ).comment;
expect(betaComment2?.comment.deleted).toBe(false);
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view); assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
}); });
@ -858,3 +857,26 @@ test("Dont send a comment reply to a blocked community", async () => {
blockRes = await blockCommunity(beta, newCommunityId, false); blockRes = await blockCommunity(beta, newCommunityId, false);
expect(blockRes.blocked).toBe(false); expect(blockRes.blocked).toBe(false);
}); });
/// Fetching a deeply nested comment can lead to stack overflow as all parent comments are also
/// fetched recursively. Ensure that it works properly.
test.skip("Fetch a deeply nested comment", async () => {
let lastComment;
for (let i = 0; i < 50; i++) {
let commentRes = await createComment(
alpha,
postOnAlphaRes.post_view.post.id,
lastComment?.comment_view.comment.id,
);
expect(commentRes.comment_view.comment).toBeDefined();
lastComment = commentRes;
}
let betaComment = await resolveComment(
beta,
lastComment!.comment_view.comment,
);
expect(betaComment!.comment!.comment).toBeDefined();
expect(betaComment?.comment?.post).toBeDefined();
});

View file

@ -527,12 +527,12 @@ test("Content in local-only community doesn't federate", async () => {
// cant resolve the community from another instance // cant resolve the community from another instance
await expect( await expect(
resolveCommunity(beta, communityRes.actor_id), resolveCommunity(beta, communityRes.actor_id),
).rejects.toStrictEqual(Error("couldnt_find_object")); ).rejects.toStrictEqual(Error("not_found"));
// create a post, also cant resolve it // create a post, also cant resolve it
let postRes = await createPost(alpha, communityRes.id); let postRes = await createPost(alpha, communityRes.id);
await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual( await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual(
Error("couldnt_find_object"), Error("not_found"),
); );
}); });

View file

@ -125,12 +125,12 @@ test("Create a post", async () => {
// Delta only follows beta, so it should not see an alpha ap_id // Delta only follows beta, so it should not see an alpha ap_id
await expect( await expect(
resolvePost(delta, postRes.post_view.post), resolvePost(delta, postRes.post_view.post),
).rejects.toStrictEqual(Error("couldnt_find_object")); ).rejects.toStrictEqual(Error("not_found"));
// Epsilon has alpha blocked, it should not see the alpha post // Epsilon has alpha blocked, it should not see the alpha post
await expect( await expect(
resolvePost(epsilon, postRes.post_view.post), resolvePost(epsilon, postRes.post_view.post),
).rejects.toStrictEqual(Error("couldnt_find_object")); ).rejects.toStrictEqual(Error("not_found"));
// remove added allow/blocklists // remove added allow/blocklists
editSiteForm.allowed_instances = []; editSiteForm.allowed_instances = [];
@ -140,9 +140,7 @@ test("Create a post", async () => {
}); });
test("Create a post in a non-existent community", async () => { test("Create a post in a non-existent community", async () => {
await expect(createPost(alpha, -2)).rejects.toStrictEqual( await expect(createPost(alpha, -2)).rejects.toStrictEqual(Error("not_found"));
Error("couldnt_find_community"),
);
}); });
test("Unlike a post", async () => { test("Unlike a post", async () => {
@ -502,10 +500,17 @@ test("Enforce site ban federation for local user", async () => {
alpha, alpha,
alphaPerson.person.id, alphaPerson.person.id,
false, false,
false, true,
); );
expect(unBanAlpha.banned).toBe(false); expect(unBanAlpha.banned).toBe(false);
// existing alpha post should be restored on beta
betaBanRes = await waitUntil(
() => getPost(beta, searchBeta1.post.id),
s => !s.post_view.post.removed,
);
expect(betaBanRes.post_view.post.removed).toBe(false);
// Login gets invalidated by ban, need to login again // Login gets invalidated by ban, need to login again
if (!alphaUserPerson) { if (!alphaUserPerson) {
throw "Missing alpha person"; throw "Missing alpha person";
@ -623,7 +628,7 @@ test("Enforce community ban for federated user", async () => {
// Alpha tries to make post on beta, but it fails because of ban // Alpha tries to make post on beta, but it fails because of ban
await expect( await expect(
createPost(alpha, betaCommunity.community.id), createPost(alpha, betaCommunity.community.id),
).rejects.toStrictEqual(Error("banned_from_community")); ).rejects.toStrictEqual(Error("person_is_banned_from_community"));
// Unban alpha // Unban alpha
let unBanAlpha = await banPersonFromCommunity( let unBanAlpha = await banPersonFromCommunity(
@ -789,3 +794,29 @@ test("Fetch post with redirect", async () => {
let gammaPost2 = await gamma.resolveObject(form); let gammaPost2 = await gamma.resolveObject(form);
expect(gammaPost2.post).toBeDefined(); expect(gammaPost2.post).toBeDefined();
}); });
test("Rewrite markdown links", async () => {
const community = (await resolveBetaCommunity(beta)).community!;
// create a post
let postRes1 = await createPost(beta, community.community.id);
// link to this post in markdown
let postRes2 = await createPost(
beta,
community.community.id,
"https://example.com/",
`[link](${postRes1.post_view.post.ap_id})`,
);
console.log(postRes2.post_view.post.body);
expect(postRes2.post_view.post).toBeDefined();
// fetch both posts from another instance
const alphaPost1 = await resolvePost(alpha, postRes1.post_view.post);
const alphaPost2 = await resolvePost(alpha, postRes2.post_view.post);
// remote markdown link is replaced with local link
expect(alphaPost2.post?.post.body).toBe(
`[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`,
);
});

View file

@ -0,0 +1,214 @@
jest.setTimeout(120000);
import { FollowCommunity } from "lemmy-js-client";
import {
alpha,
setupLogins,
createCommunity,
unfollows,
registerUser,
listCommunityPendingFollows,
getCommunity,
getCommunityPendingFollowsCount,
approveCommunityPendingFollow,
randomString,
createPost,
createComment,
beta,
resolveCommunity,
betaUrl,
resolvePost,
resolveComment,
likeComment,
waitUntil,
} from "./shared";
beforeAll(setupLogins);
afterAll(unfollows);
test("Follow a private community", async () => {
// create private community
const community = await createCommunity(alpha, randomString(10), "Private");
expect(community.community_view.community.visibility).toBe("Private");
const alphaCommunityId = community.community_view.community.id;
// No pending follows yet
const pendingFollows0 = await listCommunityPendingFollows(alpha);
expect(pendingFollows0.items.length).toBe(0);
const pendingFollowsCount0 = await getCommunityPendingFollowsCount(
alpha,
alphaCommunityId,
);
expect(pendingFollowsCount0.count).toBe(0);
// follow as new user
const user = await registerUser(beta, betaUrl);
const betaCommunity = (
await resolveCommunity(user, community.community_view.community.actor_id)
).community;
expect(betaCommunity).toBeDefined();
const betaCommunityId = betaCommunity!.community.id;
const follow_form: FollowCommunity = {
community_id: betaCommunityId,
follow: true,
};
await user.followCommunity(follow_form);
// Follow listed as pending
const follow1 = await getCommunity(user, betaCommunityId);
expect(follow1.community_view.subscribed).toBe("ApprovalRequired");
// Wait for follow to federate, shown as pending
let pendingFollows1 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
expect(pendingFollows1.items[0].is_new_instance).toBe(true);
const pendingFollowsCount1 = await getCommunityPendingFollowsCount(
alpha,
alphaCommunityId,
);
expect(pendingFollowsCount1.count).toBe(1);
// user still sees approval required at this point
const betaCommunity2 = await getCommunity(user, betaCommunityId);
expect(betaCommunity2.community_view.subscribed).toBe("ApprovalRequired");
// Approve the follow
const approve = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows1.items[0].person.id,
);
expect(approve.success).toBe(true);
// Follow is confirmed
await waitUntil(
() => getCommunity(user, betaCommunityId),
c => c.community_view.subscribed == "Subscribed",
);
const pendingFollows2 = await listCommunityPendingFollows(alpha);
expect(pendingFollows2.items.length).toBe(0);
const pendingFollowsCount2 = await getCommunityPendingFollowsCount(
alpha,
alphaCommunityId,
);
expect(pendingFollowsCount2.count).toBe(0);
// follow with another user from that instance, is_new_instance should be false now
const user2 = await registerUser(beta, betaUrl);
await user2.followCommunity(follow_form);
let pendingFollows3 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
expect(pendingFollows3.items[0].is_new_instance).toBe(false);
// cleanup pending follow
const approve2 = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows3.items[0].person.id,
);
expect(approve2.success).toBe(true);
});
test("Only followers can view and interact with private community content", async () => {
// create private community
const community = await createCommunity(alpha, randomString(10), "Private");
expect(community.community_view.community.visibility).toBe("Private");
const alphaCommunityId = community.community_view.community.id;
// create post and comment
const post0 = await createPost(alpha, alphaCommunityId);
const post_id = post0.post_view.post.id;
expect(post_id).toBeDefined();
const comment = await createComment(alpha, post_id);
const comment_id = comment.comment_view.comment.id;
expect(comment_id).toBeDefined();
// user is not following the community and cannot view nor create posts
const user = await registerUser(beta, betaUrl);
const betaCommunity = (
await resolveCommunity(user, community.community_view.community.actor_id)
).community!.community;
await expect(resolvePost(user, post0.post_view.post)).rejects.toStrictEqual(
Error("not_found"),
);
await expect(
resolveComment(user, comment.comment_view.comment),
).rejects.toStrictEqual(Error("not_found"));
await expect(createPost(user, betaCommunity.id)).rejects.toStrictEqual(
Error("not_found"),
);
// follow the community and approve
const follow_form: FollowCommunity = {
community_id: betaCommunity.id,
follow: true,
};
await user.followCommunity(follow_form);
const pendingFollows1 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
const approve = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows1.items[0].person.id,
);
expect(approve.success).toBe(true);
// now user can fetch posts and comments in community (using signed fetch), and create posts
await waitUntil(
() => resolvePost(user, post0.post_view.post),
p => p?.post?.post.id != undefined,
);
const resolvedComment = (
await resolveComment(user, comment.comment_view.comment)
).comment;
expect(resolvedComment?.comment.id).toBeDefined();
const post1 = await createPost(user, betaCommunity.id);
expect(post1.post_view).toBeDefined();
const like = await likeComment(user, 1, resolvedComment!.comment);
expect(like.comment_view.my_vote).toBe(1);
});
test("Reject follower", async () => {
// create private community
const community = await createCommunity(alpha, randomString(10), "Private");
expect(community.community_view.community.visibility).toBe("Private");
const alphaCommunityId = community.community_view.community.id;
// user is not following the community and cannot view nor create posts
const user = await registerUser(beta, betaUrl);
const betaCommunity1 = (
await resolveCommunity(user, community.community_view.community.actor_id)
).community!.community;
// follow the community and reject
const follow_form: FollowCommunity = {
community_id: betaCommunity1.id,
follow: true,
};
const follow = await user.followCommunity(follow_form);
expect(follow.community_view.subscribed).toBe("ApprovalRequired");
const pendingFollows1 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
const approve = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows1.items[0].person.id,
false,
);
expect(approve.success).toBe(true);
await waitUntil(
() => getCommunity(user, betaCommunity1.id),
c => c.community_view.subscribed == "NotSubscribed",
);
});

View file

@ -1,17 +1,24 @@
import { import {
ApproveCommunityPendingFollower,
BlockCommunity, BlockCommunity,
BlockCommunityResponse, BlockCommunityResponse,
BlockInstance, BlockInstance,
BlockInstanceResponse, BlockInstanceResponse,
CommunityId, CommunityId,
CommunityVisibility,
CreatePrivateMessageReport, CreatePrivateMessageReport,
DeleteImage, DeleteImage,
EditCommunity, EditCommunity,
GetCommunityPendingFollowsCount,
GetCommunityPendingFollowsCountResponse,
GetReplies, GetReplies,
GetRepliesResponse, GetRepliesResponse,
GetUnreadCountResponse, GetUnreadCountResponse,
InstanceId, InstanceId,
LemmyHttp, LemmyHttp,
ListCommunityPendingFollows,
ListCommunityPendingFollowsResponse,
PersonId,
PostView, PostView,
PrivateMessageReportResponse, PrivateMessageReportResponse,
SuccessResponse, SuccessResponse,
@ -83,7 +90,7 @@ export const fetchFunction = fetch;
export const imageFetchLimit = 50; export const imageFetchLimit = 50;
export const sampleImage = export const sampleImage =
"https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg"; "https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg";
export const sampleSite = "https://yahoo.com"; export const sampleSite = "https://w3.org";
export const alphaUrl = "http://127.0.0.1:8541"; export const alphaUrl = "http://127.0.0.1:8541";
export const betaUrl = "http://127.0.0.1:8551"; export const betaUrl = "http://127.0.0.1:8551";
@ -198,7 +205,7 @@ export async function setupLogins() {
// only needed the first time so do in this try // only needed the first time so do in this try
await delay(10_000); await delay(10_000);
} catch { } catch {
console.log("Communities already exist"); //console.log("Communities already exist");
} }
} }
@ -419,13 +426,13 @@ export async function banPersonFromSite(
api: LemmyHttp, api: LemmyHttp,
person_id: number, person_id: number,
ban: boolean, ban: boolean,
remove_data: boolean, remove_or_restore_data: boolean,
): Promise<BanPersonResponse> { ): Promise<BanPersonResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha // Make sure lemmy-beta/c/main is cached on lemmy_alpha
let form: BanPerson = { let form: BanPerson = {
person_id, person_id,
ban, ban,
remove_data, remove_or_restore_data,
}; };
return api.banPerson(form); return api.banPerson(form);
} }
@ -434,13 +441,13 @@ export async function banPersonFromCommunity(
api: LemmyHttp, api: LemmyHttp,
person_id: number, person_id: number,
community_id: number, community_id: number,
remove_data: boolean, remove_or_restore_data: boolean,
ban: boolean, ban: boolean,
): Promise<BanFromCommunityResponse> { ): Promise<BanFromCommunityResponse> {
let form: BanFromCommunity = { let form: BanFromCommunity = {
person_id, person_id,
community_id, community_id,
remove_data: remove_data, remove_or_restore_data,
ban, ban,
}; };
return api.banFromCommunity(form); return api.banFromCommunity(form);
@ -554,12 +561,14 @@ export async function likeComment(
export async function createCommunity( export async function createCommunity(
api: LemmyHttp, api: LemmyHttp,
name_: string = randomString(10), name_: string = randomString(10),
visibility: CommunityVisibility = "Public",
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let description = "a sample description"; let description = "a sample description";
let form: CreateCommunity = { let form: CreateCommunity = {
name: name_, name: name_,
title: name_, title: name_,
description, description,
visibility,
}; };
return api.createCommunity(form); return api.createCommunity(form);
} }
@ -688,9 +697,8 @@ export async function saveUserSettingsBio(
let form: SaveUserSettings = { let form: SaveUserSettings = {
show_nsfw: true, show_nsfw: true,
blur_nsfw: false, blur_nsfw: false,
auto_expand: true,
theme: "darkly", theme: "darkly",
default_sort_type: "Active", default_post_sort_type: "Active",
default_listing_type: "All", default_listing_type: "All",
interface_language: "en", interface_language: "en",
show_avatars: true, show_avatars: true,
@ -709,8 +717,7 @@ export async function saveUserSettingsFederated(
let form: SaveUserSettings = { let form: SaveUserSettings = {
show_nsfw: false, show_nsfw: false,
blur_nsfw: true, blur_nsfw: true,
auto_expand: false, default_post_sort_type: "Hot",
default_sort_type: "Hot",
default_listing_type: "All", default_listing_type: "All",
interface_language: "", interface_language: "",
avatar, avatar,
@ -872,6 +879,39 @@ export function blockCommunity(
return api.blockCommunity(form); return api.blockCommunity(form);
} }
export function listCommunityPendingFollows(
api: LemmyHttp,
): Promise<ListCommunityPendingFollowsResponse> {
let form: ListCommunityPendingFollows = {
pending_only: true,
all_communities: false,
page: 1,
limit: 50,
};
return api.listCommunityPendingFollows(form);
}
export function getCommunityPendingFollowsCount(
api: LemmyHttp,
community_id: CommunityId,
): Promise<GetCommunityPendingFollowsCountResponse> {
return api.getCommunityPendingFollowsCount(community_id);
}
export function approveCommunityPendingFollow(
api: LemmyHttp,
community_id: CommunityId,
follower_id: PersonId,
approve: boolean = true,
): Promise<SuccessResponse> {
let form: ApproveCommunityPendingFollower = {
community_id,
follower_id,
approve,
};
return api.approveCommunityPendingFollow(form);
}
export function delay(millis = 500) { export function delay(millis = 500) {
return new Promise(resolve => setTimeout(resolve, millis)); return new Promise(resolve => setTimeout(resolve, millis));
} }
@ -962,8 +1002,12 @@ export async function waitUntil<T>(
let retry = 0; let retry = 0;
let result; let result;
while (retry++ < retries) { while (retry++ < retries) {
result = await fetcher(); try {
if (checker(result)) return result; result = await fetcher();
if (checker(result)) return result;
} catch (error) {
//console.error(error);
}
await delay( await delay(
delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000, delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000,
); );

View file

@ -75,6 +75,8 @@
"ProxyAllImages" "ProxyAllImages"
# Timeout for uploading images to pictrs (in seconds) # Timeout for uploading images to pictrs (in seconds)
upload_timeout: 30 upload_timeout: 30
# Resize post thumbnails to this maximum width/height.
max_thumbnail_size: 512
} }
# Email sending configuration. All options except login/password are mandatory # Email sending configuration. All options except login/password are mandatory
email: { email: {

View file

@ -22,12 +22,11 @@ pub async fn distinguish_comment(
data.comment_id, data.comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
orig_comment.community.id, &orig_comment.community,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -40,7 +39,7 @@ pub async fn distinguish_comment(
// Verify that only a mod or admin can distinguish a comment // Verify that only a mod or admin can distinguish a comment
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
orig_comment.community.id, &orig_comment.community,
false, false,
&mut context.pool(), &mut context.pool(),
) )
@ -60,8 +59,7 @@ pub async fn distinguish_comment(
data.comment_id, data.comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse {
comment_view, comment_view,

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentResponse, CreateCommentLike}, comment::{CommentResponse, CreateCommentLike},
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{check_bot_account, check_community_user_action, check_downvotes_enabled}, utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::LocalUserId, newtypes::LocalUserId,
@ -27,25 +27,30 @@ pub async fn like_comment(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> LemmyResult<Json<CommentResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let comment_id = data.comment_id;
let mut recipient_ids = Vec::<LocalUserId>::new(); let mut recipient_ids = Vec::<LocalUserId>::new();
// Don't do a downvote if site has downvotes disabled check_local_vote_mode(
check_downvotes_enabled(data.score, &local_site)?; data.score,
VoteItem::Comment(comment_id),
&local_site,
local_user_view.person.id,
&mut context.pool(),
)
.await?;
check_bot_account(&local_user_view.person)?; check_bot_account(&local_user_view.person)?;
let comment_id = data.comment_id;
let orig_comment = CommentView::read( let orig_comment = CommentView::read(
&mut context.pool(), &mut context.pool(),
comment_id, comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
orig_comment.community.id, &orig_comment.community,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -54,8 +59,7 @@ pub async fn like_comment(
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await; let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await;
if let Ok(Some(reply)) = comment_reply { if let Ok(Some(reply)) = comment_reply {
let recipient_id = reply.recipient_id; let recipient_id = reply.recipient_id;
if let Ok(Some(local_recipient)) = if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await
LocalUserView::read_person(&mut context.pool(), recipient_id).await
{ {
recipient_ids.push(local_recipient.local_user.id); recipient_ids.push(local_recipient.local_user.id);
} }
@ -63,7 +67,6 @@ pub async fn like_comment(
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
comment_id: data.comment_id, comment_id: data.comment_id,
post_id: orig_comment.post.id,
person_id: local_user_view.person.id, person_id: local_user_view.person.id,
score: data.score, score: data.score,
}; };
@ -89,8 +92,7 @@ pub async fn like_comment(
score: data.score, score: data.score,
}, },
&context, &context,
) )?;
.await?;
Ok(Json( Ok(Json(
build_comment_response( build_comment_response(

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::is_mod_or_admin, utils::is_mod_or_admin,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView}; use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
/// Lists likes for a comment /// Lists likes for a comment
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -19,8 +19,7 @@ pub async fn list_comment_likes(
data.comment_id, data.comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
is_mod_or_admin( is_mod_or_admin(
&mut context.pool(), &mut context.pool(),

View file

@ -37,8 +37,7 @@ pub async fn save_comment(
comment_id, comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse {
comment_view, comment_view,

View file

@ -40,12 +40,11 @@ pub async fn create_comment_report(
comment_id, comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
comment_view.community.id, &comment_view.community,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -64,9 +63,8 @@ pub async fn create_comment_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let comment_report_view = CommentReportView::read(&mut context.pool(), report.id, person_id) let comment_report_view =
.await? CommentReportView::read(&mut context.pool(), report.id, person_id).await?;
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {
@ -87,8 +85,7 @@ pub async fn create_comment_report(
reason: data.reason.clone(), reason: data.reason.clone(),
}, },
&context, &context,
) )?;
.await?;
Ok(Json(CommentReportResponse { Ok(Json(CommentReportResponse {
comment_report_view, comment_report_view,

View file

@ -17,14 +17,12 @@ pub async fn resolve_comment_report(
) -> LemmyResult<Json<CommentReportResponse>> { ) -> LemmyResult<Json<CommentReportResponse>> {
let report_id = data.report_id; let report_id = data.report_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let report = CommentReportView::read(&mut context.pool(), report_id, person_id) let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
report.community.id, &report.community,
true, true,
&mut context.pool(), &mut context.pool(),
) )
@ -41,9 +39,8 @@ pub async fn resolve_comment_report(
} }
let report_id = data.report_id; let report_id = data.report_id;
let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id) let comment_report_view =
.await? CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
Ok(Json(CommentReportResponse { Ok(Json(CommentReportResponse {
comment_report_view, comment_report_view,

View file

@ -24,12 +24,11 @@ pub async fn add_mod_to_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<AddModToCommunityResponse>> { ) -> LemmyResult<Json<AddModToCommunityResponse>> {
let community_id = data.community_id; let community = Community::read(&mut context.pool(), data.community_id).await?;
// Verify that only mods or admins can add mod // Verify that only mods or admins can add mod
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
community_id, &community,
false, false,
&mut context.pool(), &mut context.pool(),
) )
@ -39,30 +38,23 @@ pub async fn add_mod_to_community(
if !data.added { if !data.added {
LocalUser::is_higher_mod_or_admin_check( LocalUser::is_higher_mod_or_admin_check(
&mut context.pool(), &mut context.pool(),
community_id, community.id,
local_user_view.person.id, local_user_view.person.id,
vec![data.person_id], vec![data.person_id],
) )
.await?; .await?;
} }
let community = Community::read(&mut context.pool(), community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
// If user is admin and community is remote, explicitly check that he is a // If user is admin and community is remote, explicitly check that he is a
// moderator. This is necessary because otherwise the action would be rejected // moderator. This is necessary because otherwise the action would be rejected
// by the community's home instance. // by the community's home instance.
if local_user_view.local_user.admin && !community.local { if local_user_view.local_user.admin && !community.local {
let is_mod = CommunityModeratorView::is_community_moderator( CommunityModeratorView::check_is_community_moderator(
&mut context.pool(), &mut context.pool(),
community.id, community.id,
local_user_view.person.id, local_user_view.person.id,
) )
.await?; .await?;
if !is_mod {
Err(LemmyErrorType::NotAModerator)?
}
} }
// Update in local database // Update in local database
@ -103,8 +95,7 @@ pub async fn add_mod_to_community(
added: data.added, added: data.added,
}, },
&context, &context,
) )?;
.await?;
Ok(Json(AddModToCommunityResponse { moderators })) Ok(Json(AddModToCommunityResponse { moderators }))
} }

View file

@ -4,11 +4,16 @@ use lemmy_api_common::{
community::{BanFromCommunity, BanFromCommunityResponse}, community::{BanFromCommunity, BanFromCommunityResponse},
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_mod_action, check_expire_time, remove_user_data_in_community}, utils::{
check_community_mod_action,
check_expire_time,
remove_or_restore_user_data_in_community,
},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{ community::{
Community,
CommunityFollower, CommunityFollower,
CommunityFollowerForm, CommunityFollowerForm,
CommunityPersonBan, CommunityPersonBan,
@ -33,13 +38,13 @@ pub async fn ban_from_community(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<BanFromCommunityResponse>> { ) -> LemmyResult<Json<BanFromCommunityResponse>> {
let banned_person_id = data.person_id; let banned_person_id = data.person_id;
let remove_data = data.remove_data.unwrap_or(false);
let expires = check_expire_time(data.expires)?; let expires = check_expire_time(data.expires)?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
// Verify that only mods or admins can ban // Verify that only mods or admins can ban
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
data.community_id, &community,
false, false,
&mut context.pool(), &mut context.pool(),
) )
@ -69,12 +74,7 @@ pub async fn ban_from_community(
.with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?; .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
// Also unsubscribe them from the community, if they are subscribed // Also unsubscribe them from the community, if they are subscribed
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm::new(data.community_id, banned_person_id);
community_id: data.community_id,
person_id: banned_person_id,
pending: false,
};
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
.await .await
.ok(); .ok();
@ -85,9 +85,18 @@ pub async fn ban_from_community(
} }
// Remove/Restore their data if that's desired // Remove/Restore their data if that's desired
if remove_data { if data.remove_or_restore_data.unwrap_or(false) {
remove_user_data_in_community(data.community_id, banned_person_id, &mut context.pool()).await?; let remove_data = data.ban;
} remove_or_restore_user_data_in_community(
data.community_id,
local_user_view.person.id,
banned_person_id,
remove_data,
&data.reason,
&mut context.pool(),
)
.await?;
};
// Mod tables // Mod tables
let form = ModBanFromCommunityForm { let form = ModBanFromCommunityForm {
@ -101,9 +110,7 @@ pub async fn ban_from_community(
ModBanFromCommunity::create(&mut context.pool(), &form).await?; ModBanFromCommunity::create(&mut context.pool(), &form).await?;
let person_view = PersonView::read(&mut context.pool(), data.person_id) let person_view = PersonView::read(&mut context.pool(), data.person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::BanFromCommunity { SendActivityData::BanFromCommunity {
@ -113,8 +120,7 @@ pub async fn ban_from_community(
data: data.0.clone(), data: data.0.clone(),
}, },
&context, &context,
) )?;
.await?;
Ok(Json(BanFromCommunityResponse { Ok(Json(BanFromCommunityResponse {
person_view, person_view,

View file

@ -35,12 +35,7 @@ pub async fn block_community(
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?; .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
// Also, unfollow the community, and send a federated unfollow // Also, unfollow the community, and send a federated unfollow
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm::new(data.community_id, person_id);
community_id: data.community_id,
person_id,
pending: false,
};
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
.await .await
.ok(); .ok();
@ -56,8 +51,7 @@ pub async fn block_community(
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
false, false,
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::FollowCommunity( SendActivityData::FollowCommunity(
@ -66,8 +60,7 @@ pub async fn block_community(
false, false,
), ),
&context, &context,
) )?;
.await?;
Ok(Json(BlockCommunityResponse { Ok(Json(BlockCommunityResponse {
blocked: data.block, blocked: data.block,

View file

@ -4,17 +4,18 @@ use lemmy_api_common::{
community::{CommunityResponse, FollowCommunity}, community::{CommunityResponse, FollowCommunity},
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::check_community_user_action, utils::{check_community_deleted_removed, check_user_valid},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
community::{Community, CommunityFollower, CommunityFollowerForm}, community::{Community, CommunityFollower, CommunityFollowerForm, CommunityFollowerState},
}, },
traits::{Crud, Followable}, traits::{Crud, Followable},
CommunityVisibility,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityView; use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -23,42 +24,52 @@ pub async fn follow_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> LemmyResult<Json<CommunityResponse>> {
let community = Community::read(&mut context.pool(), data.community_id) check_user_valid(&local_user_view.person)?;
.await? let community = Community::read(&mut context.pool(), data.community_id).await?;
.ok_or(LemmyErrorType::CouldntFindCommunity)?; let form = CommunityFollowerForm::new(community.id, local_user_view.person.id);
let mut community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: local_user_view.person.id,
pending: false,
};
if data.follow { if data.follow {
// Only run these checks for local community, in case of remote community the local
// state may be outdated. Can't use check_community_user_action() here as it only allows
// actions from existing followers for private community (so following would be impossible).
if community.local { if community.local {
check_community_user_action(&local_user_view.person, community.id, &mut context.pool()) check_community_deleted_removed(&community)?;
CommunityPersonBanView::check(&mut context.pool(), local_user_view.person.id, community.id)
.await?; .await?;
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
.await
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
} else {
// Mark as pending, the actual federation activity is sent via `SendActivity` handler
community_follower_form.pending = true;
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
.await
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
} }
let state = if community.local {
// Local follow is accepted immediately
Some(CommunityFollowerState::Accepted)
} else if community.visibility == CommunityVisibility::Private {
// Private communities require manual approval
Some(CommunityFollowerState::ApprovalRequired)
} else {
// remote follow needs to be federated first
Some(CommunityFollowerState::Pending)
};
let form = CommunityFollowerForm {
state,
..CommunityFollowerForm::new(community.id, local_user_view.person.id)
};
// Write to db
CommunityFollower::follow(&mut context.pool(), &form)
.await
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
} else { } else {
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) CommunityFollower::unfollow(&mut context.pool(), &form)
.await .await
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
} }
// Send the federated follow
if !community.local { if !community.local {
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow), SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow),
&context, &context,
) )?;
.await?;
} }
let community_id = data.community_id; let community_id = data.community_id;
@ -68,8 +79,7 @@ pub async fn follow_community(
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
false, false,
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;

View file

@ -48,8 +48,7 @@ pub async fn hide_community(
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::UpdateCommunity(local_user_view.person.clone(), community), SendActivityData::UpdateCommunity(local_user_view.person.clone(), community),
&context, &context,
) )?;
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -3,4 +3,6 @@ pub mod ban;
pub mod block; pub mod block;
pub mod follow; pub mod follow;
pub mod hide; pub mod hide;
pub mod pending_follows;
pub mod random;
pub mod transfer; pub mod transfer;

View file

@ -0,0 +1,46 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
community::ApproveCommunityPendingFollower,
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::is_mod_or_admin,
SuccessResponse,
};
use lemmy_db_schema::{
source::community::{CommunityFollower, CommunityFollowerForm},
traits::Followable,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
pub async fn post_pending_follows_approve(
data: Json<ApproveCommunityPendingFollower>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
is_mod_or_admin(
&mut context.pool(),
&local_user_view.person,
data.community_id,
)
.await?;
let activity_data = if data.approve {
CommunityFollower::approve(
&mut context.pool(),
data.community_id,
data.follower_id,
local_user_view.person.id,
)
.await?;
SendActivityData::AcceptFollower(data.community_id, data.follower_id)
} else {
let form = CommunityFollowerForm::new(data.community_id, data.follower_id);
CommunityFollower::unfollow(&mut context.pool(), &form).await?;
SendActivityData::RejectFollower(data.community_id, data.follower_id)
};
ActivityChannel::submit_activity(activity_data, &context)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -0,0 +1,25 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
community::{GetCommunityPendingFollowsCount, GetCommunityPendingFollowsCountResponse},
context::LemmyContext,
utils::is_mod_or_admin,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::LemmyResult;
pub async fn get_pending_follows_count(
data: Query<GetCommunityPendingFollowsCount>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<GetCommunityPendingFollowsCountResponse>> {
is_mod_or_admin(
&mut context.pool(),
&local_user_view.person,
data.community_id,
)
.await?;
let count =
CommunityFollowerView::count_approval_required(&mut context.pool(), data.community_id).await?;
Ok(Json(GetCommunityPendingFollowsCountResponse { count }))
}

View file

@ -0,0 +1,29 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
community::{ListCommunityPendingFollows, ListCommunityPendingFollowsResponse},
context::LemmyContext,
utils::check_community_mod_of_any_or_admin_action,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::LemmyResult;
pub async fn get_pending_follows_list(
data: Query<ListCommunityPendingFollows>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListCommunityPendingFollowsResponse>> {
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
let all_communities =
data.all_communities.unwrap_or_default() && local_user_view.local_user.admin;
let items = CommunityFollowerView::list_approval_required(
&mut context.pool(),
local_user_view.person.id,
all_communities,
data.pending_only.unwrap_or_default(),
data.page,
data.limit,
)
.await?;
Ok(Json(ListCommunityPendingFollowsResponse { items }))
}

View file

@ -0,0 +1,3 @@
pub mod approve;
pub mod count;
pub mod list;

View file

@ -0,0 +1,55 @@
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use lemmy_api_common::{
community::{CommunityResponse, GetRandomCommunity},
context::LemmyContext,
utils::{check_private_instance, is_mod_or_admin_opt},
};
use lemmy_db_schema::source::{
actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn get_random_community(
data: Query<GetRandomCommunity>,
context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<CommunityResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?;
let local_user = local_user_view.as_ref().map(|u| &u.local_user);
let random_community_id =
Community::get_random_community_id(&mut context.pool(), &data.type_).await?;
let is_mod_or_admin = is_mod_or_admin_opt(
&mut context.pool(),
local_user_view.as_ref(),
Some(random_community_id),
)
.await
.is_ok();
let community_view = CommunityView::read(
&mut context.pool(),
random_community_id,
local_user,
is_mod_or_admin,
)
.await?;
let discussion_languages =
CommunityLanguage::read(&mut context.pool(), random_community_id).await?;
Ok(Json(CommunityResponse {
community_view,
discussion_languages,
}))
}

View file

@ -7,7 +7,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{CommunityModerator, CommunityModeratorForm}, community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModTransferCommunity, ModTransferCommunityForm}, moderator::{ModTransferCommunity, ModTransferCommunityForm},
}, },
traits::{Crud, Joinable}, traits::{Crud, Joinable},
@ -27,11 +27,11 @@ pub async fn transfer_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetCommunityResponse>> { ) -> LemmyResult<Json<GetCommunityResponse>> {
let community_id = data.community_id; let community = Community::read(&mut context.pool(), data.community_id).await?;
let mut community_mods = let mut community_mods =
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; CommunityModeratorView::for_community(&mut context.pool(), community.id).await?;
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?; check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?;
// Make sure transferrer is either the top community mod, or an admin // Make sure transferrer is either the top community mod, or an admin
if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok()) if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok())
@ -82,13 +82,10 @@ pub async fn transfer_community(
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
false, false,
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community_id = data.community_id; let community_id = data.community_id;
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id) let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
.await
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
// Return the jwt // Return the jwt
Ok(Json(GetCommunityResponse { Ok(Json(GetCommunityResponse {

View file

@ -172,7 +172,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
target: &Person, target: &Person,
ban: bool, ban: bool,
reason: &Option<String>, reason: &Option<String>,
remove_data: &Option<bool>, remove_or_restore_data: &Option<bool>,
expires: &Option<i64>, expires: &Option<i64>,
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
@ -197,11 +197,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
.ok(); .ok();
// Also unsubscribe them from the community, if they are subscribed // Also unsubscribe them from the community, if they are subscribed
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm::new(community_id, target.id);
community_id,
person_id: target.id,
pending: false,
};
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
.await .await
@ -230,7 +226,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
person_id: target.id, person_id: target.id,
ban, ban,
reason: reason.clone(), reason: reason.clone(),
remove_data: *remove_data, remove_or_restore_data: *remove_or_restore_data,
expires: *expires, expires: *expires,
}; };
@ -242,8 +238,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
data: ban_from_community, data: ban_from_community,
}, },
context, context,
) )?;
.await?;
} }
} }
@ -258,17 +253,13 @@ pub async fn local_user_view_from_jwt(
let local_user_id = Claims::validate(jwt, context) let local_user_id = Claims::validate(jwt, context)
.await .await
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?; .with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id) let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
check_user_valid(&local_user_view.person)?; check_user_valid(&local_user_view.person)?;
Ok(local_user_view) Ok(local_user_view)
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -36,8 +36,8 @@ pub async fn add_admin(
// Make sure that the person_id added is local // Make sure that the person_id added is local
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id) let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
.await? .await
.ok_or(LemmyErrorType::ObjectNotLocal)?; .map_err(|_| LemmyErrorType::ObjectNotLocal)?;
LocalUser::update( LocalUser::update(
&mut context.pool(), &mut context.pool(),

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{BanPerson, BanPersonResponse}, person::{BanPerson, BanPersonResponse},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{check_expire_time, is_admin, remove_user_data}, utils::{check_expire_time, is_admin, remove_or_restore_user_data},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
@ -60,15 +60,22 @@ pub async fn ban_from_site(
// if its a local user, invalidate logins // if its a local user, invalidate logins
let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await; let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await;
if let Ok(Some(local_user)) = local_user { if let Ok(local_user) = local_user {
LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?; LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?;
} }
// Remove their data if that's desired // Remove their data if that's desired
let remove_data = data.remove_data.unwrap_or(false); if data.remove_or_restore_data.unwrap_or(false) {
if remove_data { let removed = data.ban;
remove_user_data(person.id, &context).await?; remove_or_restore_user_data(
} local_user_view.person.id,
person.id,
removed,
&data.reason,
&context,
)
.await?;
};
// Mod tables // Mod tables
let form = ModBanForm { let form = ModBanForm {
@ -81,16 +88,14 @@ pub async fn ban_from_site(
ModBan::create(&mut context.pool(), &form).await?; ModBan::create(&mut context.pool(), &form).await?;
let person_view = PersonView::read(&mut context.pool(), person.id) let person_view = PersonView::read(&mut context.pool(), person.id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ban_nonlocal_user_from_local_communities( ban_nonlocal_user_from_local_communities(
&local_user_view, &local_user_view,
&person, &person,
data.ban, data.ban,
&data.reason, &data.reason,
&data.remove_data, &data.remove_or_restore_data,
&data.expires, &data.expires,
&context, &context,
) )
@ -101,13 +106,12 @@ pub async fn ban_from_site(
moderator: local_user_view.person, moderator: local_user_view.person,
banned_user: person_view.person.clone(), banned_user: person_view.person.clone(),
reason: data.reason.clone(), reason: data.reason.clone(),
remove_data: data.remove_data, remove_or_restore_data: data.remove_or_restore_data,
ban: data.ban, ban: data.ban,
expires: data.expires, expires: data.expires,
}, },
&context, &context,
) )?;
.await?;
Ok(Json(BanPersonResponse { Ok(Json(BanPersonResponse {
person_view, person_view,

View file

@ -32,8 +32,7 @@ pub async fn block_person(
let target_user = LocalUserView::read_person(&mut context.pool(), target_id) let target_user = LocalUserView::read_person(&mut context.pool(), target_id)
.await .await
.ok() .ok();
.flatten();
if target_user.is_some_and(|t| t.local_user.admin) { if target_user.is_some_and(|t| t.local_user.admin) {
Err(LemmyErrorType::CantBlockAdmin)? Err(LemmyErrorType::CantBlockAdmin)?
@ -49,9 +48,7 @@ pub async fn block_person(
.with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?; .with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?;
} }
let person_view = PersonView::read(&mut context.pool(), target_id) let person_view = PersonView::read(&mut context.pool(), target_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
Ok(Json(BlockPersonResponse { Ok(Json(BlockPersonResponse {
person_view, person_view,
blocked: data.block, blocked: data.block,

View file

@ -28,11 +28,13 @@ pub async fn change_password(
} }
// Check the old password // Check the old password
let valid: bool = verify( let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted
&data.old_password, {
&local_user_view.local_user.password_encrypted, verify(&data.old_password, password_encrypted).unwrap_or(false)
) } else {
.unwrap_or(false); data.old_password.is_empty()
};
if !valid { if !valid {
Err(LemmyErrorType::IncorrectLogin)? Err(LemmyErrorType::IncorrectLogin)?
} }

View file

@ -21,7 +21,6 @@ pub async fn change_password_after_reset(
let token = data.token.clone(); let token = data.token.clone();
let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token) let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token)
.await? .await?
.ok_or(LemmyErrorType::TokenNotFound)?
.local_user_id; .local_user_id;
password_length_check(&data.password)?; password_length_check(&data.password)?;

View file

@ -2,8 +2,11 @@ use crate::{build_totp_2fa, generate_totp_2fa_secret};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse}; use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse};
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm}; use lemmy_db_schema::source::{
use lemmy_db_views::structs::{LocalUserView, SiteView}; local_user::{LocalUser, LocalUserUpdateForm},
site::Site,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
/// Generate a new secret for two-factor-authentication. Afterwards you need to call [toggle_totp] /// Generate a new secret for two-factor-authentication. Afterwards you need to call [toggle_totp]
@ -13,17 +16,14 @@ pub async fn generate_totp_secret(
local_user_view: LocalUserView, local_user_view: LocalUserView,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GenerateTotpSecretResponse>> { ) -> LemmyResult<Json<GenerateTotpSecretResponse>> {
let site_view = SiteView::read_local(&mut context.pool()) let site = Site::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
if local_user_view.local_user.totp_2fa_enabled { if local_user_view.local_user.totp_2fa_enabled {
return Err(LemmyErrorType::TotpAlreadyEnabled)?; return Err(LemmyErrorType::TotpAlreadyEnabled)?;
} }
let secret = generate_totp_2fa_secret(); let secret = generate_totp_2fa_secret();
let secret_url = let secret_url = build_totp_2fa(&site.name, &local_user_view.person.name, &secret)?.get_url();
build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url();
let local_user_form = LocalUserUpdateForm { let local_user_form = LocalUserUpdateForm {
totp_2fa_secret: Some(Some(secret)), totp_2fa_secret: Some(Some(secret)),

View file

@ -1,5 +1,5 @@
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use lemmy_api_common::context::LemmyContext; use lemmy_api_common::{context::LemmyContext, person::ListLoginsResponse};
use lemmy_db_schema::source::login_token::LoginToken; use lemmy_db_schema::source::login_token::LoginToken;
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
@ -7,8 +7,8 @@ use lemmy_utils::error::LemmyResult;
pub async fn list_logins( pub async fn list_logins(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<Vec<LoginToken>>> { ) -> LemmyResult<Json<ListLoginsResponse>> {
let logins = LoginToken::list(&mut context.pool(), local_user_view.local_user.id).await?; let logins = LoginToken::list(&mut context.pool(), local_user_view.local_user.id).await?;
Ok(Json(logins)) Ok(Json(ListLoginsResponse { logins }))
} }

View file

@ -1,4 +1,4 @@
use crate::{check_totp_2fa_valid, local_user::check_email_verified}; use crate::check_totp_2fa_valid;
use actix_web::{ use actix_web::{
web::{Data, Json}, web::{Data, Json},
HttpRequest, HttpRequest,
@ -8,12 +8,7 @@ use lemmy_api_common::{
claims::Claims, claims::Claims,
context::LemmyContext, context::LemmyContext,
person::{Login, LoginResponse}, person::{Login, LoginResponse},
utils::check_user_valid, utils::{check_email_verified, check_registration_application, check_user_valid},
};
use lemmy_db_schema::{
source::{local_site::LocalSite, registration_application::RegistrationApplication},
utils::DbPool,
RegistrationMode,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
@ -24,23 +19,20 @@ pub async fn login(
req: HttpRequest, req: HttpRequest,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<LoginResponse>> { ) -> LemmyResult<Json<LoginResponse>> {
let site_view = SiteView::read_local(&mut context.pool()) let site_view = SiteView::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
// Fetch that username / email // Fetch that username / email
let username_or_email = data.username_or_email.clone(); let username_or_email = data.username_or_email.clone();
let local_user_view = let local_user_view =
LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email) LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email).await?;
.await?
.ok_or(LemmyErrorType::IncorrectLogin)?;
// Verify the password // Verify the password
let valid: bool = verify( let valid: bool = local_user_view
&data.password, .local_user
&local_user_view.local_user.password_encrypted, .password_encrypted
) .as_ref()
.unwrap_or(false); .and_then(|password_encrypted| verify(&data.password, password_encrypted).ok())
.unwrap_or(false);
if !valid { if !valid {
Err(LemmyErrorType::IncorrectLogin)? Err(LemmyErrorType::IncorrectLogin)?
} }
@ -67,28 +59,3 @@ pub async fn login(
registration_created: false, registration_created: false,
})) }))
} }
async fn check_registration_application(
local_user_view: &LocalUserView,
local_site: &LocalSite,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
if (local_site.registration_mode == RegistrationMode::RequireApplication
|| local_site.registration_mode == RegistrationMode::Closed)
&& !local_user_view.local_user.accepted_application
&& !local_user_view.local_user.admin
{
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
// was processed (either accepted or denied).
let local_user_id = local_user_view.local_user.id;
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
if registration.admin_id.is_some() {
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
} else {
Err(LemmyErrorType::RegistrationApplicationIsPending)?
}
}
Ok(())
}

View file

@ -1,6 +1,3 @@
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
pub mod add_admin; pub mod add_admin;
pub mod ban_person; pub mod ban_person;
pub mod block; pub mod block;
@ -20,15 +17,3 @@ pub mod save_settings;
pub mod update_totp; pub mod update_totp;
pub mod validate_auth; pub mod validate_auth;
pub mod verify_email; pub mod verify_email;
/// Check if the user's email is verified if email verification is turned on
/// However, skip checking verification if the user is an admin
fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> {
if !local_user_view.local_user.admin
&& site_view.local_site.require_email_verification
&& !local_user_view.local_user.email_verified
{
Err(LemmyErrorType::EmailNotVerified)?
}
Ok(())
}

View file

@ -18,9 +18,7 @@ pub async fn mark_person_mention_as_read(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PersonMentionResponse>> { ) -> LemmyResult<Json<PersonMentionResponse>> {
let person_mention_id = data.person_mention_id; let person_mention_id = data.person_mention_id;
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id) let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
if local_user_view.person.id != read_person_mention.recipient_id { if local_user_view.person.id != read_person_mention.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)? Err(LemmyErrorType::CouldntUpdateComment)?
@ -39,9 +37,7 @@ pub async fn mark_person_mention_as_read(
let person_mention_id = read_person_mention.id; let person_mention_id = read_person_mention.id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let person_mention_view = let person_mention_view =
PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)) PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
Ok(Json(PersonMentionResponse { Ok(Json(PersonMentionResponse {
person_mention_view, person_mention_view,

View file

@ -18,9 +18,7 @@ pub async fn mark_reply_as_read(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentReplyResponse>> { ) -> LemmyResult<Json<CommentReplyResponse>> {
let comment_reply_id = data.comment_reply_id; let comment_reply_id = data.comment_reply_id;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id) let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
if local_user_view.person.id != read_comment_reply.recipient_id { if local_user_view.person.id != read_comment_reply.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)? Err(LemmyErrorType::CouldntUpdateComment)?
@ -40,9 +38,7 @@ pub async fn mark_reply_as_read(
let comment_reply_id = read_comment_reply.id; let comment_reply_id = read_comment_reply.id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_reply_view = let comment_reply_view =
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)) CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
Ok(Json(CommentReplyResponse { comment_reply_view })) Ok(Json(CommentReplyResponse { comment_reply_view }))
} }

View file

@ -1,9 +1,8 @@
use crate::local_user::check_email_verified;
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::PasswordReset, person::PasswordReset,
utils::send_password_reset_email, utils::{check_email_verified, send_password_reset_email},
SuccessResponse, SuccessResponse,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
@ -17,12 +16,10 @@ pub async fn reset_password(
// Fetch that email // Fetch that email
let email = data.email.to_lowercase(); let email = data.email.to_lowercase();
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email) let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
.await? .await
.ok_or(LemmyErrorType::IncorrectLogin)?; .map_err(|_| LemmyErrorType::IncorrectLogin)?;
let site_view = SiteView::read_local(&mut context.pool()) let site_view = SiteView::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
check_email_verified(&local_user_view, &site_view)?; check_email_verified(&local_user_view, &site_view)?;
// Email the pure token to the user. // Email the pure token to the user.

View file

@ -36,9 +36,7 @@ pub async fn save_user_settings(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool()) let site_view = SiteView::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let slur_regex = local_site_to_slur_regex(&site_view.local_site); let slur_regex = local_site_to_slur_regex(&site_view.local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
@ -65,9 +63,7 @@ pub async fn save_user_settings(
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
// if email was changed, check that it is not taken and send verification mail // if email was changed, check that it is not taken and send verification mail
if previous_email.deref() != email { if previous_email.deref() != email {
if LocalUser::is_email_taken(&mut context.pool(), email).await? { LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
return Err(LemmyErrorType::EmailAlreadyExists)?;
}
send_verification_email( send_verification_email(
&local_user_view, &local_user_view,
email, email,
@ -104,7 +100,8 @@ pub async fn save_user_settings(
let local_user_id = local_user_view.local_user.id; let local_user_id = local_user_view.local_user.id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let default_listing_type = data.default_listing_type; let default_listing_type = data.default_listing_type;
let default_sort_type = data.default_sort_type; let default_post_sort_type = data.default_post_sort_type;
let default_comment_sort_type = data.default_comment_sort_type;
let person_form = PersonUpdateForm { let person_form = PersonUpdateForm {
display_name, display_name,
@ -133,10 +130,9 @@ pub async fn save_user_settings(
send_notifications_to_email: data.send_notifications_to_email, send_notifications_to_email: data.send_notifications_to_email,
show_nsfw: data.show_nsfw, show_nsfw: data.show_nsfw,
blur_nsfw: data.blur_nsfw, blur_nsfw: data.blur_nsfw,
auto_expand: data.auto_expand,
show_bot_accounts: data.show_bot_accounts, show_bot_accounts: data.show_bot_accounts,
show_scores: data.show_scores, default_post_sort_type,
default_sort_type, default_comment_sort_type,
default_listing_type, default_listing_type,
theme: data.theme.clone(), theme: data.theme.clone(),
interface_language: data.interface_language.clone(), interface_language: data.interface_language.clone(),
@ -145,6 +141,7 @@ pub async fn save_user_settings(
post_listing_mode: data.post_listing_mode, post_listing_mode: data.post_listing_mode,
enable_keyboard_navigation: data.enable_keyboard_navigation, enable_keyboard_navigation: data.enable_keyboard_navigation,
enable_animated_images: data.enable_animated_images, enable_animated_images: data.enable_animated_images,
enable_private_messages: data.enable_private_messages,
collapse_bot_comments: data.collapse_bot_comments, collapse_bot_comments: data.collapse_bot_comments,
..Default::default() ..Default::default()
}; };

View file

@ -10,19 +10,15 @@ use lemmy_db_schema::source::{
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::LemmyResult;
pub async fn verify_email( pub async fn verify_email(
data: Json<VerifyEmail>, data: Json<VerifyEmail>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool()) let site_view = SiteView::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let token = data.token.clone(); let token = data.token.clone();
let verification = EmailVerification::read_for_token(&mut context.pool(), &token) let verification = EmailVerification::read_for_token(&mut context.pool(), &token).await?;
.await?
.ok_or(LemmyErrorType::TokenNotFound)?;
let form = LocalUserUpdateForm { let form = LocalUserUpdateForm {
// necessary in case this is a new signup // necessary in case this is a new signup
@ -39,9 +35,7 @@ pub async fn verify_email(
// send out notification about registration application to admins if enabled // send out notification about registration application to admins if enabled
if site_view.local_site.application_email_admins { if site_view.local_site.application_email_admins {
let local_user = LocalUserView::read(&mut context.pool(), local_user_id) let local_user = LocalUserView::read(&mut context.pool(), local_user_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
send_new_applicant_email_to_admins( send_new_applicant_email_to_admins(
&local_user.person.name, &local_user.person.name,

View file

@ -9,6 +9,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::Community,
moderator::{ModFeaturePost, ModFeaturePostForm}, moderator::{ModFeaturePost, ModFeaturePostForm},
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },
@ -16,7 +17,7 @@ use lemmy_db_schema::{
PostFeatureType, PostFeatureType,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn feature_post( pub async fn feature_post(
@ -25,13 +26,12 @@ pub async fn feature_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id) let orig_post = Post::read(&mut context.pool(), post_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let community = Community::read(&mut context.pool(), orig_post.community_id).await?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
orig_post.community_id, &community,
false, false,
&mut context.pool(), &mut context.pool(),
) )
@ -69,8 +69,7 @@ pub async fn feature_post(
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::FeaturePost(post, local_user_view.person.clone(), data.featured), SendActivityData::FeaturePost(post, local_user_view.person.clone(), data.featured),
&context, &context,
) )?;
.await?;
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
} }

View file

@ -5,10 +5,7 @@ use lemmy_api_common::{
request::fetch_link_metadata, request::fetch_link_metadata,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{ use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
error::{LemmyErrorExt, LemmyResult},
LemmyErrorType,
};
use url::Url; use url::Url;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]

View file

@ -8,19 +8,19 @@ use lemmy_api_common::{
utils::{ utils::{
check_bot_account, check_bot_account,
check_community_user_action, check_community_user_action,
check_downvotes_enabled, check_local_vote_mode,
mark_post_as_read, mark_post_as_read,
VoteItem,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::Community,
local_site::LocalSite, local_site::LocalSite,
post::{Post, PostLike, PostLikeForm}, post::{PostLike, PostLikeForm},
}, },
traits::{Crud, Likeable}, traits::Likeable,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use std::ops::Deref; use std::ops::Deref;
@ -31,20 +31,24 @@ pub async fn like_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let post_id = data.post_id;
// Don't do a downvote if site has downvotes disabled check_local_vote_mode(
check_downvotes_enabled(data.score, &local_site)?; data.score,
VoteItem::Post(post_id),
&local_site,
local_user_view.person.id,
&mut context.pool(),
)
.await?;
check_bot_account(&local_user_view.person)?; check_bot_account(&local_user_view.person)?;
// Check for a community ban // Check for a community ban
let post_id = data.post_id; let post = PostView::read(&mut context.pool(), post_id, None, false).await?;
let post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
post.community_id, &post.community,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -70,20 +74,15 @@ pub async fn like_post(
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let community = Community::read(&mut context.pool(), post.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment { SendActivityData::LikePostOrComment {
object_id: post.ap_id, object_id: post.post.ap_id,
actor: local_user_view.person.clone(), actor: local_user_view.person.clone(),
community, community: post.community.clone(),
score: data.score, score: data.score,
}, },
&context, &context,
) )?;
.await?;
build_post_response(context.deref(), post.community_id, local_user_view, post_id).await build_post_response(context.deref(), post.community.id, local_user_view, post_id).await
} }

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{source::post::Post, traits::Crud}; use lemmy_db_schema::{source::post::Post, traits::Crud};
use lemmy_db_views::structs::{LocalUserView, VoteView}; use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
/// Lists likes for a post /// Lists likes for a post
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -15,9 +15,7 @@ pub async fn list_post_likes(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<ListPostLikesResponse>> { ) -> LemmyResult<Json<ListPostLikesResponse>> {
let post = Post::read(&mut context.pool(), data.post_id) let post = Post::read(&mut context.pool(), data.post_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
is_mod_or_admin( is_mod_or_admin(
&mut context.pool(), &mut context.pool(),
&local_user_view.person, &local_user_view.person,

View file

@ -14,8 +14,8 @@ use lemmy_db_schema::{
}, },
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn lock_post( pub async fn lock_post(
@ -24,13 +24,11 @@ pub async fn lock_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id) let orig_post = PostView::read(&mut context.pool(), post_id, None, false).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
orig_post.community_id, &orig_post.community,
false, false,
&mut context.pool(), &mut context.pool(),
) )
@ -60,8 +58,7 @@ pub async fn lock_post(
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::LockPost(post, local_user_view.person.clone(), data.locked), SendActivityData::LockPost(post, local_user_view.person.clone(), data.locked),
&context, &context,
) )?;
.await?;
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await build_post_response(&context, orig_post.community.id, local_user_view, post_id).await
} }

View file

@ -40,8 +40,7 @@ pub async fn save_post(
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
false, false,
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindPost)?;
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;

View file

@ -35,13 +35,11 @@ pub async fn create_post_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_id = data.post_id; let post_id = data.post_id;
let post_view = PostView::read(&mut context.pool(), post_id, None, false) let post_view = PostView::read(&mut context.pool(), post_id, None, false).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
post_view.community.id, &post_view.community,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -61,9 +59,7 @@ pub async fn create_post_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id) let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {
@ -84,8 +80,7 @@ pub async fn create_post_report(
reason: data.reason.clone(), reason: data.reason.clone(),
}, },
&context, &context,
) )?;
.await?;
Ok(Json(PostReportResponse { post_report_view })) Ok(Json(PostReportResponse { post_report_view }))
} }

View file

@ -17,14 +17,12 @@ pub async fn resolve_post_report(
) -> LemmyResult<Json<PostReportResponse>> { ) -> LemmyResult<Json<PostReportResponse>> {
let report_id = data.report_id; let report_id = data.report_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let report = PostReportView::read(&mut context.pool(), report_id, person_id) let report = PostReportView::read(&mut context.pool(), report_id, person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
report.community.id, &report.community,
true, true,
&mut context.pool(), &mut context.pool(),
) )
@ -40,9 +38,7 @@ pub async fn resolve_post_report(
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
} }
let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id) let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
Ok(Json(PostReportResponse { post_report_view })) Ok(Json(PostReportResponse { post_report_view }))
} }

View file

@ -18,9 +18,7 @@ pub async fn mark_pm_as_read(
) -> LemmyResult<Json<PrivateMessageResponse>> { ) -> LemmyResult<Json<PrivateMessageResponse>> {
// Checking permissions // Checking permissions
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id) let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
if local_user_view.person.id != orig_private_message.recipient_id { if local_user_view.person.id != orig_private_message.recipient_id {
Err(LemmyErrorType::CouldntUpdatePrivateMessage)? Err(LemmyErrorType::CouldntUpdatePrivateMessage)?
} }
@ -39,9 +37,7 @@ pub async fn mark_pm_as_read(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id) let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
Ok(Json(PrivateMessageResponse { Ok(Json(PrivateMessageResponse {
private_message_view: view, private_message_view: view,
})) }))

View file

@ -29,9 +29,7 @@ pub async fn create_pm_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id) let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
// Make sure that only the recipient of the private message can create a report // Make sure that only the recipient of the private message can create a report
if person_id != private_message.recipient_id { if person_id != private_message.recipient_id {
@ -49,9 +47,8 @@ pub async fn create_pm_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report.id) let private_message_report_view =
.await? PrivateMessageReportView::read(&mut context.pool(), report.id).await?;
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {

View file

@ -28,9 +28,8 @@ pub async fn resolve_pm_report(
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
} }
let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report_id) let private_message_report_view =
.await? PrivateMessageReportView::read(&mut context.pool(), report_id).await?;
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
Ok(Json(PrivateMessageReportResponse { Ok(Json(PrivateMessageReportResponse {
private_message_report_view, private_message_report_view,

View file

@ -5,15 +5,13 @@ use lemmy_api_common::{
utils::build_federated_instances, utils::build_federated_instances,
}; };
use lemmy_db_views::structs::SiteView; use lemmy_db_views::structs::SiteView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn get_federated_instances( pub async fn get_federated_instances(
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GetFederatedInstancesResponse>> { ) -> LemmyResult<Json<GetFederatedInstancesResponse>> {
let site_view = SiteView::read_local(&mut context.pool()) let site_view = SiteView::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let federated_instances = let federated_instances =
build_federated_instances(&site_view.local_site, &mut context.pool()).await?; build_federated_instances(&site_view.local_site, &mut context.pool()).await?;

View file

@ -7,11 +7,12 @@ use lemmy_db_schema::{
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm}, moderator::{ModAdd, ModAddForm},
oauth_provider::OAuthProvider,
tagline::Tagline, tagline::Tagline,
}, },
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_views_actor::structs::PersonView; use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorType, LemmyResult}, error::{LemmyErrorType, LemmyResult},
@ -55,17 +56,14 @@ pub async fn leave_admin(
ModAdd::create(&mut context.pool(), &form).await?; ModAdd::create(&mut context.pool(), &form).await?;
// Reread site and admins // Reread site and admins
let site_view = SiteView::read_local(&mut context.pool()) let site_view = SiteView::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let admins = PersonView::admins(&mut context.pool()).await?; let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; let oauth_providers = OAuthProvider::get_all_public(&mut context.pool()).await?;
let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
Ok(Json(GetSiteResponse { Ok(Json(GetSiteResponse {
site_view, site_view,
@ -74,8 +72,11 @@ pub async fn leave_admin(
my_user: None, my_user: None,
all_languages, all_languages,
discussion_languages, discussion_languages,
taglines, oauth_providers: Some(oauth_providers),
custom_emojis, admin_oauth_providers: None,
blocked_urls, blocked_urls,
tagline,
taglines: vec![],
custom_emojis: vec![],
})) }))
} }

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_comment( pub async fn purge_comment(
@ -35,8 +35,7 @@ pub async fn purge_comment(
comment_id, comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
// Also check that you're a higher admin // Also check that you're a higher admin
LocalUser::is_higher_admin_check( LocalUser::is_higher_admin_check(
@ -68,8 +67,7 @@ pub async fn purge_comment(
reason: data.reason.clone(), reason: data.reason.clone(),
}, },
&context, &context,
) )?;
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -19,7 +19,7 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_db_views_actor::structs::CommunityModeratorView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_community( pub async fn purge_community(
@ -31,9 +31,7 @@ pub async fn purge_community(
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Read the community to get its images // Read the community to get its images
let community = Community::read(&mut context.pool(), data.community_id) let community = Community::read(&mut context.pool(), data.community_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
// Also check that you're a higher admin than all the mods // Also check that you're a higher admin than all the mods
let community_mod_person_ids = let community_mod_person_ids =
@ -77,8 +75,7 @@ pub async fn purge_community(
removed: true, removed: true,
}, },
&context, &context,
) )?;
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -17,7 +17,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_person( pub async fn purge_person(
@ -36,9 +36,7 @@ pub async fn purge_person(
) )
.await?; .await?;
let person = Person::read(&mut context.pool(), data.person_id) let person = Person::read(&mut context.pool(), data.person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ban_nonlocal_user_from_local_communities( ban_nonlocal_user_from_local_communities(
&local_user_view, &local_user_view,
@ -77,13 +75,12 @@ pub async fn purge_person(
moderator: local_user_view.person, moderator: local_user_view.person,
banned_user: person, banned_user: person,
reason: data.reason.clone(), reason: data.reason.clone(),
remove_data: Some(true), remove_or_restore_data: Some(true),
ban: true, ban: true,
expires: None, expires: None,
}, },
&context, &context,
) )?;
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -17,7 +17,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_post( pub async fn purge_post(
@ -29,9 +29,7 @@ pub async fn purge_post(
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Read the post to get the community_id // Read the post to get the community_id
let post = Post::read(&mut context.pool(), data.post_id) let post = Post::read(&mut context.pool(), data.post_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Also check that you're a higher admin // Also check that you're a higher admin
LocalUser::is_higher_admin_check( LocalUser::is_higher_admin_check(
@ -68,8 +66,7 @@ pub async fn purge_post(
removed: true, removed: true,
}, },
&context, &context,
) )?;
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -14,10 +14,7 @@ use lemmy_db_schema::{
utils::{diesel_string_update, get_conn}, utils::{diesel_string_update, get_conn},
}; };
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
use lemmy_utils::{ use lemmy_utils::error::{LemmyError, LemmyResult};
error::{LemmyError, LemmyResult},
LemmyErrorType,
};
pub async fn approve_registration_application( pub async fn approve_registration_application(
data: Json<ApproveRegistrationApplication>, data: Json<ApproveRegistrationApplication>,
@ -61,9 +58,8 @@ pub async fn approve_registration_application(
.await?; .await?;
if data.approve { if data.approve {
let approved_local_user_view = LocalUserView::read(&mut context.pool(), approved_user_id) let approved_local_user_view =
.await? LocalUserView::read(&mut context.pool(), approved_user_id).await?;
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
if approved_local_user_view.local_user.email.is_some() { if approved_local_user_view.local_user.email.is_some() {
// Email sending may fail, but this won't revert the application approval // Email sending may fail, but this won't revert the application approval
send_application_approved_email(&approved_local_user_view, context.settings()).await?; send_application_approved_email(&approved_local_user_view, context.settings()).await?;
@ -71,9 +67,8 @@ pub async fn approve_registration_application(
}; };
// Read the view // Read the view
let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id) let registration_application =
.await? RegistrationApplicationView::read(&mut context.pool(), app_id).await?;
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
Ok(Json(RegistrationApplicationResponse { Ok(Json(RegistrationApplicationResponse {
registration_application, registration_application,

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::is_admin, utils::is_admin,
}; };
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
/// Lists registration applications, filterable by undenied only. /// Lists registration applications, filterable by undenied only.
pub async fn get_registration_application( pub async fn get_registration_application(
@ -18,9 +18,7 @@ pub async fn get_registration_application(
// Read the view // Read the view
let registration_application = let registration_application =
RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id) RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
Ok(Json(RegistrationApplicationResponse { Ok(Json(RegistrationApplicationResponse {
registration_application, registration_application,

View file

@ -31,16 +31,16 @@ use lemmy_db_schema::{
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType, CACHE_DURATION_API}; use lemmy_utils::{
error::{LemmyErrorType, LemmyResult},
CACHE_DURATION_API,
};
use serial_test::serial; use serial_test::serial;
#[allow(clippy::unwrap_used)]
async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance, LocalUserView)> { async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance, LocalUserView)> {
let pool = &mut context.pool(); let pool = &mut context.pool();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
.await
.expect("Create test instance");
let admin_person = Person::create( let admin_person = Person::create(
pool, pool,
@ -54,35 +54,26 @@ async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance
) )
.await?; .await?;
let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id) let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id).await?;
.await?
.unwrap();
let site_form = SiteInsertForm::builder() let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id);
.name("test site".to_string()) let site = Site::create(pool, &site_form).await?;
.instance_id(inserted_instance.id)
.build();
let site = Site::create(pool, &site_form).await.unwrap();
// Create a local site, since this is necessary for determining if email verification is // Create a local site, since this is necessary for determining if email verification is
// required // required
let local_site_form = LocalSiteInsertForm::builder() let local_site_form = LocalSiteInsertForm {
.site_id(site.id) require_email_verification: Some(true),
.require_email_verification(Some(true)) application_question: Some(".".to_string()),
.application_question(Some(".".to_string())) registration_mode: Some(RegistrationMode::RequireApplication),
.registration_mode(Some(RegistrationMode::RequireApplication)) site_setup: Some(true),
.site_setup(Some(true)) ..LocalSiteInsertForm::new(site.id)
.build(); };
let local_site = LocalSite::create(pool, &local_site_form).await.unwrap(); let local_site = LocalSite::create(pool, &local_site_form).await?;
// Required to have a working local SiteView when updating the site to change email verification // Required to have a working local SiteView when updating the site to change email verification
// requirement or registration mode // requirement or registration mode
let rate_limit_form = LocalSiteRateLimitInsertForm::builder() let rate_limit_form = LocalSiteRateLimitInsertForm::new(local_site.id);
.local_site_id(local_site.id) LocalSiteRateLimit::create(pool, &rate_limit_form).await?;
.build();
LocalSiteRateLimit::create(pool, &rate_limit_form)
.await
.unwrap();
Ok((inserted_instance, admin_local_user_view)) Ok((inserted_instance, admin_local_user_view))
} }
@ -116,7 +107,6 @@ async fn signup(
Ok((local_user, application)) Ok((local_user, application))
} }
#[allow(clippy::unwrap_used)]
async fn get_application_statuses( async fn get_application_statuses(
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
admin: LocalUserView, admin: LocalUserView,
@ -129,14 +119,14 @@ async fn get_application_statuses(
get_unread_registration_application_count(context.reset_request_count(), admin.clone()).await?; get_unread_registration_application_count(context.reset_request_count(), admin.clone()).await?;
let unread_applications = list_registration_applications( let unread_applications = list_registration_applications(
Query::from_query("unread_only=true").unwrap(), Query::from_query("unread_only=true")?,
context.reset_request_count(), context.reset_request_count(),
admin.clone(), admin.clone(),
) )
.await?; .await?;
let all_applications = list_registration_applications( let all_applications = list_registration_applications(
Query::from_query("unread_only=false").unwrap(), Query::from_query("unread_only=false")?,
context.reset_request_count(), context.reset_request_count(),
admin, admin,
) )
@ -145,10 +135,9 @@ async fn get_application_statuses(
Ok((application_count, unread_applications, all_applications)) Ok((application_count, unread_applications, all_applications))
} }
#[allow(clippy::indexing_slicing)]
#[allow(clippy::unwrap_used)]
#[tokio::test]
#[serial] #[serial]
#[tokio::test]
#[expect(clippy::indexing_slicing)]
async fn test_application_approval() -> LemmyResult<()> { async fn test_application_approval() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool(); let pool = &mut context.pool();

View file

@ -9,14 +9,12 @@ use lemmy_utils::error::LemmyResult;
use sitemap_rs::{url::Url, url_set::UrlSet}; use sitemap_rs::{url::Url, url_set::UrlSet};
use tracing::info; use tracing::info;
async fn generate_urlset( fn generate_urlset(posts: Vec<(DbUrl, chrono::DateTime<chrono::Utc>)>) -> LemmyResult<UrlSet> {
posts: Vec<(DbUrl, chrono::DateTime<chrono::Utc>)>,
) -> LemmyResult<UrlSet> {
let urls = posts let urls = posts
.into_iter() .into_iter()
.map_while(|post| { .map_while(|(url, date_time)| {
Url::builder(post.0.to_string()) Url::builder(url.to_string())
.last_modified(post.1.into()) .last_modified(date_time.into())
.build() .build()
.ok() .ok()
}) })
@ -31,7 +29,7 @@ pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
info!("Loaded latest {} posts", posts.len()); info!("Loaded latest {} posts", posts.len());
let mut buf = Vec::<u8>::new(); let mut buf = Vec::<u8>::new();
generate_urlset(posts).await?.write(&mut buf)?; generate_urlset(posts)?.write(&mut buf)?;
Ok( Ok(
HttpResponse::Ok() HttpResponse::Ok()
@ -42,44 +40,40 @@ pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
pub(crate) mod tests { pub(crate) mod tests {
use crate::sitemap::generate_urlset; use crate::sitemap::generate_urlset;
use chrono::{DateTime, NaiveDate, Utc}; use chrono::{DateTime, NaiveDate, Utc};
use elementtree::Element; use elementtree::Element;
use lemmy_db_schema::newtypes::DbUrl; use lemmy_db_schema::newtypes::DbUrl;
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use url::Url; use url::Url;
#[tokio::test] #[tokio::test]
async fn test_generate_urlset() { async fn test_generate_urlset() -> LemmyResult<()> {
let posts: Vec<(DbUrl, DateTime<Utc>)> = vec![ let posts: Vec<(DbUrl, DateTime<Utc>)> = vec![
( (
Url::parse("https://example.com").unwrap().into(), Url::parse("https://example.com")?.into(),
NaiveDate::from_ymd_opt(2022, 12, 1) NaiveDate::from_ymd_opt(2022, 12, 1)
.unwrap() .unwrap_or_default()
.and_hms_opt(9, 10, 11) .and_hms_opt(9, 10, 11)
.unwrap() .unwrap_or_default()
.and_utc(), .and_utc(),
), ),
( (
Url::parse("https://lemmy.ml").unwrap().into(), Url::parse("https://lemmy.ml")?.into(),
NaiveDate::from_ymd_opt(2023, 1, 1) NaiveDate::from_ymd_opt(2023, 1, 1)
.unwrap() .unwrap_or_default()
.and_hms_opt(1, 2, 3) .and_hms_opt(1, 2, 3)
.unwrap() .unwrap_or_default()
.and_utc(), .and_utc(),
), ),
]; ];
let mut buf = Vec::<u8>::new(); let mut buf = Vec::<u8>::new();
generate_urlset(posts) generate_urlset(posts)?.write(&mut buf)?;
.await let root = Element::from_reader(buf.as_slice())?;
.unwrap()
.write(&mut buf)
.unwrap();
let root = Element::from_reader(buf.as_slice()).unwrap();
assert_eq!(root.tag().name(), "urlset"); assert_eq!(root.tag().name(), "urlset");
assert_eq!(root.child_count(), 2); assert_eq!(root.child_count(), 2);
@ -99,45 +93,43 @@ pub(crate) mod tests {
root root
.children() .children()
.next() .next()
.unwrap() .and_then(|n| n.children().find(|element| element.tag().name() == "loc"))
.children() .map(Element::text)
.find(|element| element.tag().name() == "loc") .unwrap_or_default(),
.unwrap()
.text(),
"https://example.com/" "https://example.com/"
); );
assert_eq!( assert_eq!(
root root
.children() .children()
.next() .next()
.unwrap() .and_then(|n| n
.children() .children()
.find(|element| element.tag().name() == "lastmod") .find(|element| element.tag().name() == "lastmod"))
.unwrap() .map(Element::text)
.text(), .unwrap_or_default(),
"2022-12-01T09:10:11+00:00" "2022-12-01T09:10:11+00:00"
); );
assert_eq!( assert_eq!(
root root
.children() .children()
.nth(1) .nth(1)
.unwrap() .and_then(|n| n.children().find(|element| element.tag().name() == "loc"))
.children() .map(Element::text)
.find(|element| element.tag().name() == "loc") .unwrap_or_default(),
.unwrap()
.text(),
"https://lemmy.ml/" "https://lemmy.ml/"
); );
assert_eq!( assert_eq!(
root root
.children() .children()
.nth(1) .nth(1)
.unwrap() .and_then(|n| n
.children() .children()
.find(|element| element.tag().name() == "lastmod") .find(|element| element.tag().name() == "lastmod"))
.unwrap() .map(Element::text)
.text(), .unwrap_or_default(),
"2023-01-01T01:02:03+00:00" "2023-01-01T01:02:03+00:00"
); );
Ok(())
} }
} }

View file

@ -72,7 +72,7 @@ jsonwebtoken = { version = "9.3.0", optional = true }
# necessary for wasmt compilation # necessary for wasmt compilation
getrandom = { version = "0.2.15", features = ["js"] } getrandom = { version = "0.2.15", features = ["js"] }
[package.metadata.cargo-machete] [package.metadata.cargo-shear]
ignored = ["getrandom"] ignored = ["getrandom"]
[dev-dependencies] [dev-dependencies]

View file

@ -27,7 +27,6 @@ use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::{ use lemmy_utils::{
error::LemmyResult, error::LemmyResult,
utils::{markdown::markdown_to_html, mention::MentionData}, utils::{markdown::markdown_to_html, mention::MentionData},
LemmyErrorType,
}; };
pub async fn build_comment_response( pub async fn build_comment_response(
@ -37,9 +36,8 @@ pub async fn build_comment_response(
recipient_ids: Vec<LocalUserId>, recipient_ids: Vec<LocalUserId>,
) -> LemmyResult<CommentResponse> { ) -> LemmyResult<CommentResponse> {
let local_user = local_user_view.map(|l| l.local_user); let local_user = local_user_view.map(|l| l.local_user);
let comment_view = CommentView::read(&mut context.pool(), comment_id, local_user.as_ref()) let comment_view =
.await? CommentView::read(&mut context.pool(), comment_id, local_user.as_ref()).await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(CommentResponse { Ok(CommentResponse {
comment_view, comment_view,
recipient_ids, recipient_ids,
@ -61,8 +59,7 @@ pub async fn build_community_response(
Some(&local_user), Some(&local_user),
is_mod_or_admin, is_mod_or_admin,
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
Ok(Json(CommunityResponse { Ok(Json(CommunityResponse {
@ -87,8 +84,7 @@ pub async fn build_post_response(
Some(&local_user), Some(&local_user),
is_mod_or_admin, is_mod_or_admin,
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindPost)?;
Ok(Json(PostResponse { post_view })) Ok(Json(PostResponse { post_view }))
} }
@ -112,8 +108,7 @@ pub async fn send_local_notifs(
comment_id, comment_id,
local_user_view.map(|view| &view.local_user), local_user_view.map(|view| &view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
let comment = comment_view.comment; let comment = comment_view.comment;
let post = comment_view.post; let post = comment_view.post;
let community = comment_view.community; let community = comment_view.community;
@ -125,7 +120,7 @@ pub async fn send_local_notifs(
{ {
let mention_name = mention.name.clone(); let mention_name = mention.name.clone();
let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await; let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await;
if let Ok(Some(mention_user_view)) = user_view { if let Ok(mention_user_view) = user_view {
// TODO // TODO
// At some point, make it so you can't tag the parent creator either // At some point, make it so you can't tag the parent creator either
// Potential duplication of notifications, one for reply and the other for mention, is handled // Potential duplication of notifications, one for reply and the other for mention, is handled
@ -161,9 +156,7 @@ pub async fn send_local_notifs(
// Send comment_reply to the parent commenter / poster // Send comment_reply to the parent commenter / poster
if let Some(parent_comment_id) = comment.parent_comment_id() { if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id) let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
// Get the parent commenter local_user // Get the parent commenter local_user
let parent_creator_id = parent_comment.creator_id; let parent_creator_id = parent_comment.creator_id;
@ -182,7 +175,7 @@ pub async fn send_local_notifs(
// Don't send a notif to yourself // Don't send a notif to yourself
if parent_comment.creator_id != person.id && !check_blocks { if parent_comment.creator_id != person.id && !check_blocks {
let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await;
if let Ok(Some(parent_user_view)) = user_view { if let Ok(parent_user_view) = user_view {
// Don't duplicate notif if already mentioned by checking recipient ids // Don't duplicate notif if already mentioned by checking recipient ids
if !recipient_ids.contains(&parent_user_view.local_user.id) { if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);
@ -229,7 +222,7 @@ pub async fn send_local_notifs(
if post.creator_id != person.id && !check_blocks { if post.creator_id != person.id && !check_blocks {
let creator_id = post.creator_id; let creator_id = post.creator_id;
let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await;
if let Ok(Some(parent_user_view)) = parent_user { if let Ok(parent_user_view) = parent_user {
if !recipient_ids.contains(&parent_user_view.local_user.id) { if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);

View file

@ -29,12 +29,8 @@ impl Claims {
let claims = let claims =
decode::<Claims>(jwt, &key, &validation).with_lemmy_type(LemmyErrorType::NotLoggedIn)?; decode::<Claims>(jwt, &key, &validation).with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let user_id = LocalUserId(claims.claims.sub.parse()?); let user_id = LocalUserId(claims.claims.sub.parse()?);
let is_valid = LoginToken::validate(&mut context.pool(), user_id, jwt).await?; LoginToken::validate(&mut context.pool(), user_id, jwt).await?;
if !is_valid { Ok(user_id)
Err(LemmyErrorType::NotLoggedIn)?
} else {
Ok(user_id)
}
} }
pub async fn generate( pub async fn generate(
@ -73,8 +69,6 @@ impl Claims {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{claims::Claims, context::LemmyContext}; use crate::{claims::Claims, context::LemmyContext};
@ -89,7 +83,7 @@ mod tests {
traits::Crud, traits::Crud,
utils::build_db_pool_for_tests, utils::build_db_pool_for_tests,
}; };
use lemmy_utils::rate_limit::RateLimitCell; use lemmy_utils::{error::LemmyResult, rate_limit::RateLimitCell};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use reqwest::Client; use reqwest::Client;
use reqwest_middleware::ClientBuilder; use reqwest_middleware::ClientBuilder;
@ -97,10 +91,10 @@ mod tests {
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_should_not_validate_user_token_after_password_change() { async fn test_should_not_validate_user_token_after_password_change() -> LemmyResult<()> {
let pool_ = build_db_pool_for_tests().await; let pool_ = build_db_pool_for_tests();
let pool = &mut (&pool_).into(); let pool = &mut (&pool_).into();
let secret = Secret::init(pool).await.unwrap().unwrap(); let secret = Secret::init(pool).await?;
let context = LemmyContext::create( let context = LemmyContext::create(
pool_.clone(), pool_.clone(),
ClientBuilder::new(Client::default()).build(), ClientBuilder::new(Client::default()).build(),
@ -108,29 +102,25 @@ mod tests {
RateLimitCell::with_test_config(), RateLimitCell::with_test_config(),
); );
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
.await
.unwrap();
let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812"); let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812");
let inserted_person = Person::create(pool, &new_person).await.unwrap(); let inserted_person = Person::create(pool, &new_person).await?;
let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let local_user_form = LocalUserInsertForm::test_form(inserted_person.id);
let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?;
.await
.unwrap();
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let jwt = Claims::generate(inserted_local_user.id, req, &context) let jwt = Claims::generate(inserted_local_user.id, req, &context).await?;
.await
.unwrap();
let valid = Claims::validate(&jwt, &context).await; let valid = Claims::validate(&jwt, &context).await;
assert!(valid.is_ok()); assert!(valid.is_ok());
let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); let num_deleted = Person::delete(pool, inserted_person.id).await?;
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
Ok(())
} }
} }

View file

@ -17,7 +17,9 @@ use ts_rs::TS;
pub struct CreateComment { pub struct CreateComment {
pub content: String, pub content: String,
pub post_id: PostId, pub post_id: PostId,
#[cfg_attr(feature = "full", ts(optional))]
pub parent_id: Option<CommentId>, pub parent_id: Option<CommentId>,
#[cfg_attr(feature = "full", ts(optional))]
pub language_id: Option<LanguageId>, pub language_id: Option<LanguageId>,
} }
@ -37,7 +39,9 @@ pub struct GetComment {
/// Edit a comment. /// Edit a comment.
pub struct EditComment { pub struct EditComment {
pub comment_id: CommentId, pub comment_id: CommentId,
#[cfg_attr(feature = "full", ts(optional))]
pub content: Option<String>, pub content: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub language_id: Option<LanguageId>, pub language_id: Option<LanguageId>,
} }
@ -69,6 +73,7 @@ pub struct DeleteComment {
pub struct RemoveComment { pub struct RemoveComment {
pub comment_id: CommentId, pub comment_id: CommentId,
pub removed: bool, pub removed: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
} }
@ -107,17 +112,29 @@ pub struct CreateCommentLike {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Get a list of comments. /// Get a list of comments.
pub struct GetComments { pub struct GetComments {
#[cfg_attr(feature = "full", ts(optional))]
pub type_: Option<ListingType>, pub type_: Option<ListingType>,
#[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<CommentSortType>, pub sort: Option<CommentSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub max_depth: Option<i32>, pub max_depth: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_name: Option<String>, pub community_name: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_id: Option<PostId>, pub post_id: Option<PostId>,
#[cfg_attr(feature = "full", ts(optional))]
pub parent_id: Option<CommentId>, pub parent_id: Option<CommentId>,
#[cfg_attr(feature = "full", ts(optional))]
pub saved_only: Option<bool>, pub saved_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub liked_only: Option<bool>, pub liked_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub disliked_only: Option<bool>, pub disliked_only: Option<bool>,
} }
@ -161,12 +178,17 @@ pub struct ResolveCommentReport {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// List comment reports. /// List comment reports.
pub struct ListCommentReports { pub struct ListCommentReports {
#[cfg_attr(feature = "full", ts(optional))]
pub comment_id: Option<CommentId>, pub comment_id: Option<CommentId>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
/// Only shows the unresolved reports /// Only shows the unresolved reports
#[cfg_attr(feature = "full", ts(optional))]
pub unresolved_only: Option<bool>, pub unresolved_only: Option<bool>,
/// if no community is given, it returns reports for all communities moderated by the auth user /// if no community is given, it returns reports for all communities moderated by the auth user
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
} }
@ -185,7 +207,9 @@ pub struct ListCommentReportsResponse {
/// List comment likes. Admins-only. /// List comment likes. Admins-only.
pub struct ListCommentLikes { pub struct ListCommentLikes {
pub comment_id: CommentId, pub comment_id: CommentId,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
} }

View file

@ -3,9 +3,14 @@ use lemmy_db_schema::{
source::site::Site, source::site::Site,
CommunityVisibility, CommunityVisibility,
ListingType, ListingType,
SortType,
}; };
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView, PersonView}; use lemmy_db_views_actor::structs::{
CommunityModeratorView,
CommunitySortType,
CommunityView,
PendingFollow,
PersonView,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
#[cfg(feature = "full")] #[cfg(feature = "full")]
@ -15,10 +20,13 @@ use ts_rs::TS;
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
// TODO make this into a tagged enum
/// Get a community. Must provide either an id, or a name. /// Get a community. Must provide either an id, or a name.
pub struct GetCommunity { pub struct GetCommunity {
#[cfg_attr(feature = "full", ts(optional))]
pub id: Option<CommunityId>, pub id: Option<CommunityId>,
/// Example: star_trek , or star_trek@xyz.tld /// Example: star_trek , or star_trek@xyz.tld
#[cfg_attr(feature = "full", ts(optional))]
pub name: Option<String>, pub name: Option<String>,
} }
@ -29,6 +37,7 @@ pub struct GetCommunity {
/// The community response. /// The community response.
pub struct GetCommunityResponse { pub struct GetCommunityResponse {
pub community_view: CommunityView, pub community_view: CommunityView,
#[cfg_attr(feature = "full", ts(optional))]
pub site: Option<Site>, pub site: Option<Site>,
pub moderators: Vec<CommunityModeratorView>, pub moderators: Vec<CommunityModeratorView>,
pub discussion_languages: Vec<LanguageId>, pub discussion_languages: Vec<LanguageId>,
@ -44,17 +53,27 @@ pub struct CreateCommunity {
pub name: String, pub name: String,
/// A longer title. /// A longer title.
pub title: String, pub title: String,
/// A longer sidebar, or description of your community, in markdown. /// A sidebar for the community in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub sidebar: Option<String>,
/// A shorter, one line description of your community.
#[cfg_attr(feature = "full", ts(optional))]
pub description: Option<String>, pub description: Option<String>,
/// An icon URL. /// An icon URL.
#[cfg_attr(feature = "full", ts(optional))]
pub icon: Option<String>, pub icon: Option<String>,
/// A banner URL. /// A banner URL.
#[cfg_attr(feature = "full", ts(optional))]
pub banner: Option<String>, pub banner: Option<String>,
/// Whether its an NSFW community. /// Whether its an NSFW community.
#[cfg_attr(feature = "full", ts(optional))]
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
/// Whether to restrict posting only to moderators. /// Whether to restrict posting only to moderators.
#[cfg_attr(feature = "full", ts(optional))]
pub posting_restricted_to_mods: Option<bool>, pub posting_restricted_to_mods: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub discussion_languages: Option<Vec<LanguageId>>, pub discussion_languages: Option<Vec<LanguageId>>,
#[cfg_attr(feature = "full", ts(optional))]
pub visibility: Option<CommunityVisibility>, pub visibility: Option<CommunityVisibility>,
} }
@ -73,10 +92,15 @@ pub struct CommunityResponse {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Fetches a list of communities. /// Fetches a list of communities.
pub struct ListCommunities { pub struct ListCommunities {
#[cfg_attr(feature = "full", ts(optional))]
pub type_: Option<ListingType>, pub type_: Option<ListingType>,
pub sort: Option<SortType>, #[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<CommunitySortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub show_nsfw: Option<bool>, pub show_nsfw: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
} }
@ -97,11 +121,16 @@ pub struct BanFromCommunity {
pub community_id: CommunityId, pub community_id: CommunityId,
pub person_id: PersonId, pub person_id: PersonId,
pub ban: bool, pub ban: bool,
pub remove_data: Option<bool>, /// Optionally remove or restore all their data. Useful for new troll accounts.
/// If ban is true, then this means remove. If ban is false, it means restore.
#[cfg_attr(feature = "full", ts(optional))]
pub remove_or_restore_data: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
/// A time that the ban will expire, in unix epoch seconds. /// A time that the ban will expire, in unix epoch seconds.
/// ///
/// An i64 unix timestamp is used for a simpler API client implementation. /// An i64 unix timestamp is used for a simpler API client implementation.
#[cfg_attr(feature = "full", ts(optional))]
pub expires: Option<i64>, pub expires: Option<i64>,
} }
@ -140,18 +169,29 @@ pub struct AddModToCommunityResponse {
pub struct EditCommunity { pub struct EditCommunity {
pub community_id: CommunityId, pub community_id: CommunityId,
/// A longer title. /// A longer title.
#[cfg_attr(feature = "full", ts(optional))]
pub title: Option<String>, pub title: Option<String>,
/// A longer sidebar, or description of your community, in markdown. /// A sidebar for the community in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub sidebar: Option<String>,
/// A shorter, one line description of your community.
#[cfg_attr(feature = "full", ts(optional))]
pub description: Option<String>, pub description: Option<String>,
/// An icon URL. /// An icon URL.
#[cfg_attr(feature = "full", ts(optional))]
pub icon: Option<String>, pub icon: Option<String>,
/// A banner URL. /// A banner URL.
#[cfg_attr(feature = "full", ts(optional))]
pub banner: Option<String>, pub banner: Option<String>,
/// Whether its an NSFW community. /// Whether its an NSFW community.
#[cfg_attr(feature = "full", ts(optional))]
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
/// Whether to restrict posting only to moderators. /// Whether to restrict posting only to moderators.
#[cfg_attr(feature = "full", ts(optional))]
pub posting_restricted_to_mods: Option<bool>, pub posting_restricted_to_mods: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub discussion_languages: Option<Vec<LanguageId>>, pub discussion_languages: Option<Vec<LanguageId>>,
#[cfg_attr(feature = "full", ts(optional))]
pub visibility: Option<CommunityVisibility>, pub visibility: Option<CommunityVisibility>,
} }
@ -163,6 +203,7 @@ pub struct EditCommunity {
pub struct HideCommunity { pub struct HideCommunity {
pub community_id: CommunityId, pub community_id: CommunityId,
pub hidden: bool, pub hidden: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
} }
@ -184,6 +225,7 @@ pub struct DeleteCommunity {
pub struct RemoveCommunity { pub struct RemoveCommunity {
pub community_id: CommunityId, pub community_id: CommunityId,
pub removed: bool, pub removed: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
} }
@ -223,3 +265,60 @@ pub struct TransferCommunity {
pub community_id: CommunityId, pub community_id: CommunityId,
pub person_id: PersonId, pub person_id: PersonId,
} }
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetches a random community
pub struct GetRandomCommunity {
#[cfg_attr(feature = "full", ts(optional))]
pub type_: Option<ListingType>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ListCommunityPendingFollows {
/// Only shows the unapproved applications
#[cfg_attr(feature = "full", ts(optional))]
pub pending_only: Option<bool>,
// Only for admins, show pending follows for communities which you dont moderate
#[cfg_attr(feature = "full", ts(optional))]
pub all_communities: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct GetCommunityPendingFollowsCount {
pub community_id: CommunityId,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct GetCommunityPendingFollowsCountResponse {
pub count: i64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ListCommunityPendingFollowsResponse {
pub items: Vec<PendingFollow>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ApproveCommunityPendingFollower {
pub community_id: CommunityId,
pub follower_id: PersonId,
pub approve: bool,
}

View file

@ -57,7 +57,7 @@ impl LemmyContext {
/// Do not use this in production code. /// Do not use this in production code.
pub async fn init_test_federation_config() -> FederationConfig<LemmyContext> { pub async fn init_test_federation_config() -> FederationConfig<LemmyContext> {
// call this to run migrations // call this to run migrations
let pool = build_db_pool_for_tests().await; let pool = build_db_pool_for_tests();
let client = client_builder(&SETTINGS).build().expect("build client"); let client = client_builder(&SETTINGS).build().expect("build client");

View file

@ -1,6 +1,7 @@
use lemmy_db_schema::newtypes::CustomEmojiId; use lemmy_db_schema::newtypes::CustomEmojiId;
use lemmy_db_views::structs::CustomEmojiView; use lemmy_db_views::structs::CustomEmojiView;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")] #[cfg(feature = "full")]
use ts_rs::TS; use ts_rs::TS;
use url::Url; use url::Url;
@ -46,3 +47,27 @@ pub struct DeleteCustomEmoji {
pub struct CustomEmojiResponse { pub struct CustomEmojiResponse {
pub custom_emoji: CustomEmojiView, pub custom_emoji: CustomEmojiView,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A response for custom emojis.
pub struct ListCustomEmojisResponse {
pub custom_emojis: Vec<CustomEmojiView>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetches a list of custom emojis.
pub struct ListCustomEmojis {
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub category: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub ignore_page_limits: Option<bool>,
}

View file

@ -7,6 +7,7 @@ pub mod community;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod context; pub mod context;
pub mod custom_emoji; pub mod custom_emoji;
pub mod oauth_provider;
pub mod person; pub mod person;
pub mod post; pub mod post;
pub mod private_message; pub mod private_message;
@ -15,6 +16,7 @@ pub mod request;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod send_activity; pub mod send_activity;
pub mod site; pub mod site;
pub mod tagline;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod utils; pub mod utils;
@ -24,7 +26,7 @@ pub extern crate lemmy_db_views_actor;
pub extern crate lemmy_db_views_moderator; pub extern crate lemmy_db_views_moderator;
pub extern crate lemmy_utils; pub extern crate lemmy_utils;
pub use lemmy_utils::LemmyErrorType; pub use lemmy_utils::error::LemmyErrorType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{cmp::min, time::Duration}; use std::{cmp::min, time::Duration};

View file

@ -0,0 +1,85 @@
use lemmy_db_schema::newtypes::OAuthProviderId;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
use url::Url;
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create an external auth method.
pub struct CreateOAuthProvider {
pub display_name: String,
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
pub userinfo_endpoint: String,
pub id_claim: String,
pub client_id: String,
pub client_secret: String,
pub scopes: String,
#[cfg_attr(feature = "full", ts(optional))]
pub auto_verify_email: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub account_linking_enabled: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub enabled: Option<bool>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Edit an external auth method.
pub struct EditOAuthProvider {
pub id: OAuthProviderId,
#[cfg_attr(feature = "full", ts(optional))]
pub display_name: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub authorization_endpoint: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub token_endpoint: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub userinfo_endpoint: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub id_claim: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub client_secret: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub scopes: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub auto_verify_email: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub account_linking_enabled: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub enabled: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete an external auth method.
pub struct DeleteOAuthProvider {
pub id: OAuthProviderId,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Logging in with an OAuth 2.0 authorization
pub struct AuthenticateWithOauth {
pub code: String,
pub oauth_provider_id: OAuthProviderId,
pub redirect_uri: Url,
#[cfg_attr(feature = "full", ts(optional))]
pub show_nsfw: Option<bool>,
/// Username is mandatory at registration time
#[cfg_attr(feature = "full", ts(optional))]
pub username: Option<String>,
/// An answer is mandatory if require application is enabled on the server
#[cfg_attr(feature = "full", ts(optional))]
pub answer: Option<String>,
}

View file

@ -1,11 +1,11 @@
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
sensitive::SensitiveString, sensitive::SensitiveString,
source::site::Site, source::{login_token::LoginToken, site::Site},
CommentSortType, CommentSortType,
ListingType, ListingType,
PostListingMode, PostListingMode,
SortType, PostSortType,
}; };
use lemmy_db_views::structs::{CommentView, LocalImageView, PostView}; use lemmy_db_views::structs::{CommentView, LocalImageView, PostView};
use lemmy_db_views_actor::structs::{ use lemmy_db_views_actor::structs::{
@ -28,6 +28,7 @@ pub struct Login {
pub username_or_email: SensitiveString, pub username_or_email: SensitiveString,
pub password: SensitiveString, pub password: SensitiveString,
/// May be required, if totp is enabled for their account. /// May be required, if totp is enabled for their account.
#[cfg_attr(feature = "full", ts(optional))]
pub totp_2fa_token: Option<String>, pub totp_2fa_token: Option<String>,
} }
@ -40,16 +41,22 @@ pub struct Register {
pub username: String, pub username: String,
pub password: SensitiveString, pub password: SensitiveString,
pub password_verify: SensitiveString, pub password_verify: SensitiveString,
#[cfg_attr(feature = "full", ts(optional))]
pub show_nsfw: Option<bool>, pub show_nsfw: Option<bool>,
/// email is mandatory if email verification is enabled on the server /// email is mandatory if email verification is enabled on the server
#[cfg_attr(feature = "full", ts(optional))]
pub email: Option<SensitiveString>, pub email: Option<SensitiveString>,
/// The UUID of the captcha item. /// The UUID of the captcha item.
#[cfg_attr(feature = "full", ts(optional))]
pub captcha_uuid: Option<String>, pub captcha_uuid: Option<String>,
/// Your captcha answer. /// Your captcha answer.
#[cfg_attr(feature = "full", ts(optional))]
pub captcha_answer: Option<String>, pub captcha_answer: Option<String>,
/// A form field to trick signup bots. Should be None. /// A form field to trick signup bots. Should be None.
#[cfg_attr(feature = "full", ts(optional))]
pub honeypot: Option<String>, pub honeypot: Option<String>,
/// An answer is mandatory if require application is enabled on the server /// An answer is mandatory if require application is enabled on the server
#[cfg_attr(feature = "full", ts(optional))]
pub answer: Option<String>, pub answer: Option<String>,
} }
@ -60,6 +67,7 @@ pub struct Register {
/// A wrapper for the captcha response. /// A wrapper for the captcha response.
pub struct GetCaptchaResponse { pub struct GetCaptchaResponse {
/// Will be None if captchas are disabled. /// Will be None if captchas are disabled.
#[cfg_attr(feature = "full", ts(optional))]
pub ok: Option<CaptchaResponse>, pub ok: Option<CaptchaResponse>,
} }
@ -83,56 +91,92 @@ pub struct CaptchaResponse {
/// Saves settings for your user. /// Saves settings for your user.
pub struct SaveUserSettings { pub struct SaveUserSettings {
/// Show nsfw posts. /// Show nsfw posts.
#[cfg_attr(feature = "full", ts(optional))]
pub show_nsfw: Option<bool>, pub show_nsfw: Option<bool>,
/// Blur nsfw posts.
#[cfg_attr(feature = "full", ts(optional))]
pub blur_nsfw: Option<bool>, pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
/// Your user's theme. /// Your user's theme.
#[cfg_attr(feature = "full", ts(optional))]
pub theme: Option<String>, pub theme: Option<String>,
pub default_sort_type: Option<SortType>, /// The default post listing type, usually "local"
#[cfg_attr(feature = "full", ts(optional))]
pub default_listing_type: Option<ListingType>, pub default_listing_type: Option<ListingType>,
/// A post-view mode that changes how multiple post listings look.
#[cfg_attr(feature = "full", ts(optional))]
pub post_listing_mode: Option<PostListingMode>,
/// The default post sort, usually "active"
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_sort_type: Option<PostSortType>,
/// The default comment sort, usually "hot"
#[cfg_attr(feature = "full", ts(optional))]
pub default_comment_sort_type: Option<CommentSortType>,
/// The language of the lemmy interface /// The language of the lemmy interface
#[cfg_attr(feature = "full", ts(optional))]
pub interface_language: Option<String>, pub interface_language: Option<String>,
/// A URL for your avatar. /// A URL for your avatar.
#[cfg_attr(feature = "full", ts(optional))]
pub avatar: Option<String>, pub avatar: Option<String>,
/// A URL for your banner. /// A URL for your banner.
#[cfg_attr(feature = "full", ts(optional))]
pub banner: Option<String>, pub banner: Option<String>,
/// Your display name, which can contain strange characters, and does not need to be unique. /// Your display name, which can contain strange characters, and does not need to be unique.
#[cfg_attr(feature = "full", ts(optional))]
pub display_name: Option<String>, pub display_name: Option<String>,
/// Your email. /// Your email.
#[cfg_attr(feature = "full", ts(optional))]
pub email: Option<SensitiveString>, pub email: Option<SensitiveString>,
/// Your bio / info, in markdown. /// Your bio / info, in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub bio: Option<String>, pub bio: Option<String>,
/// Your matrix user id. Ex: @my_user:matrix.org /// Your matrix user id. Ex: @my_user:matrix.org
#[cfg_attr(feature = "full", ts(optional))]
pub matrix_user_id: Option<String>, pub matrix_user_id: Option<String>,
/// Whether to show or hide avatars. /// Whether to show or hide avatars.
#[cfg_attr(feature = "full", ts(optional))]
pub show_avatars: Option<bool>, pub show_avatars: Option<bool>,
/// Sends notifications to your email. /// Sends notifications to your email.
#[cfg_attr(feature = "full", ts(optional))]
pub send_notifications_to_email: Option<bool>, pub send_notifications_to_email: Option<bool>,
/// Whether this account is a bot account. Users can hide these accounts easily if they wish. /// Whether this account is a bot account. Users can hide these accounts easily if they wish.
#[cfg_attr(feature = "full", ts(optional))]
pub bot_account: Option<bool>, pub bot_account: Option<bool>,
/// Whether to show bot accounts. /// Whether to show bot accounts.
#[cfg_attr(feature = "full", ts(optional))]
pub show_bot_accounts: Option<bool>, pub show_bot_accounts: Option<bool>,
/// Whether to show read posts. /// Whether to show read posts.
#[cfg_attr(feature = "full", ts(optional))]
pub show_read_posts: Option<bool>, pub show_read_posts: Option<bool>,
/// A list of languages you are able to see discussion in. /// A list of languages you are able to see discussion in.
#[cfg_attr(feature = "full", ts(optional))]
pub discussion_languages: Option<Vec<LanguageId>>, pub discussion_languages: Option<Vec<LanguageId>>,
/// Open links in a new tab /// Open links in a new tab
#[cfg_attr(feature = "full", ts(optional))]
pub open_links_in_new_tab: Option<bool>, pub open_links_in_new_tab: Option<bool>,
/// Enable infinite scroll /// Enable infinite scroll
#[cfg_attr(feature = "full", ts(optional))]
pub infinite_scroll_enabled: Option<bool>, pub infinite_scroll_enabled: Option<bool>,
/// A post-view mode that changes how multiple post listings look.
pub post_listing_mode: Option<PostListingMode>,
/// Whether to allow keyboard navigation (for browsing and interacting with posts and comments). /// Whether to allow keyboard navigation (for browsing and interacting with posts and comments).
#[cfg_attr(feature = "full", ts(optional))]
pub enable_keyboard_navigation: Option<bool>, pub enable_keyboard_navigation: Option<bool>,
/// Whether user avatars or inline images in the UI that are gifs should be allowed to play or /// Whether user avatars or inline images in the UI that are gifs should be allowed to play or
/// should be paused /// should be paused
#[cfg_attr(feature = "full", ts(optional))]
pub enable_animated_images: Option<bool>, pub enable_animated_images: Option<bool>,
/// Whether a user can send / receive private messages
#[cfg_attr(feature = "full", ts(optional))]
pub enable_private_messages: Option<bool>,
/// Whether to auto-collapse bot comments. /// Whether to auto-collapse bot comments.
#[cfg_attr(feature = "full", ts(optional))]
pub collapse_bot_comments: Option<bool>, pub collapse_bot_comments: Option<bool>,
/// Some vote display mode settings /// Some vote display mode settings
#[cfg_attr(feature = "full", ts(optional))]
pub show_scores: Option<bool>, pub show_scores: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub show_upvotes: Option<bool>, pub show_upvotes: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub show_downvotes: Option<bool>, pub show_downvotes: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub show_upvote_percentage: Option<bool>, pub show_upvote_percentage: Option<bool>,
} }
@ -154,6 +198,7 @@ pub struct ChangePassword {
pub struct LoginResponse { pub struct LoginResponse {
/// This is None in response to `Register` if email verification is enabled, or the server /// This is None in response to `Register` if email verification is enabled, or the server
/// requires registration applications. /// requires registration applications.
#[cfg_attr(feature = "full", ts(optional))]
pub jwt: Option<SensitiveString>, pub jwt: Option<SensitiveString>,
/// If registration applications are required, this will return true for a signup response. /// If registration applications are required, this will return true for a signup response.
pub registration_created: bool, pub registration_created: bool,
@ -169,13 +214,20 @@ pub struct LoginResponse {
/// ///
/// Either person_id, or username are required. /// Either person_id, or username are required.
pub struct GetPersonDetails { pub struct GetPersonDetails {
#[cfg_attr(feature = "full", ts(optional))]
pub person_id: Option<PersonId>, pub person_id: Option<PersonId>,
/// Example: dessalines , or dessalines@xyz.tld /// Example: dessalines , or dessalines@xyz.tld
#[cfg_attr(feature = "full", ts(optional))]
pub username: Option<String>, pub username: Option<String>,
pub sort: Option<SortType>, #[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<PostSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
#[cfg_attr(feature = "full", ts(optional))]
pub saved_only: Option<bool>, pub saved_only: Option<bool>,
} }
@ -186,6 +238,7 @@ pub struct GetPersonDetails {
/// A person's details response. /// A person's details response.
pub struct GetPersonDetailsResponse { pub struct GetPersonDetailsResponse {
pub person_view: PersonView, pub person_view: PersonView,
#[cfg_attr(feature = "full", ts(optional))]
pub site: Option<Site>, pub site: Option<Site>,
pub comments: Vec<CommentView>, pub comments: Vec<CommentView>,
pub posts: Vec<PostView>, pub posts: Vec<PostView>,
@ -217,12 +270,16 @@ pub struct AddAdminResponse {
pub struct BanPerson { pub struct BanPerson {
pub person_id: PersonId, pub person_id: PersonId,
pub ban: bool, pub ban: bool,
/// Optionally remove all their data. Useful for new troll accounts. /// Optionally remove or restore all their data. Useful for new troll accounts.
pub remove_data: Option<bool>, /// If ban is true, then this means remove. If ban is false, it means restore.
#[cfg_attr(feature = "full", ts(optional))]
pub remove_or_restore_data: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
/// A time that the ban will expire, in unix epoch seconds. /// A time that the ban will expire, in unix epoch seconds.
/// ///
/// An i64 unix timestamp is used for a simpler API client implementation. /// An i64 unix timestamp is used for a simpler API client implementation.
#[cfg_attr(feature = "full", ts(optional))]
pub expires: Option<i64>, pub expires: Option<i64>,
} }
@ -268,9 +325,13 @@ pub struct BlockPersonResponse {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Get comment replies. /// Get comment replies.
pub struct GetReplies { pub struct GetReplies {
#[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<CommentSortType>, pub sort: Option<CommentSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub unread_only: Option<bool>, pub unread_only: Option<bool>,
} }
@ -289,9 +350,13 @@ pub struct GetRepliesResponse {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Get mentions for your user. /// Get mentions for your user.
pub struct GetPersonMentions { pub struct GetPersonMentions {
#[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<CommentSortType>, pub sort: Option<CommentSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub unread_only: Option<bool>, pub unread_only: Option<bool>,
} }
@ -370,6 +435,7 @@ pub struct PasswordChangeAfterReset {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Get a count of the number of reports. /// Get a count of the number of reports.
pub struct GetReportCount { pub struct GetReportCount {
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
} }
@ -379,9 +445,11 @@ pub struct GetReportCount {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// A response for the number of reports. /// A response for the number of reports.
pub struct GetReportCountResponse { pub struct GetReportCountResponse {
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
pub comment_reports: i64, pub comment_reports: i64,
pub post_reports: i64, pub post_reports: i64,
#[cfg_attr(feature = "full", ts(optional))]
pub private_message_reports: Option<i64>, pub private_message_reports: Option<i64>,
} }
@ -431,7 +499,9 @@ pub struct UpdateTotpResponse {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Get your user's image / media uploads. /// Get your user's image / media uploads.
pub struct ListMedia { pub struct ListMedia {
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
} }
@ -441,3 +511,10 @@ pub struct ListMedia {
pub struct ListMediaResponse { pub struct ListMediaResponse {
pub images: Vec<LocalImageView>, pub images: Vec<LocalImageView>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ListLoginsResponse {
pub logins: Vec<LoginToken>,
}

View file

@ -2,7 +2,7 @@ use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId}, newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId},
ListingType, ListingType,
PostFeatureType, PostFeatureType,
SortType, PostSortType,
}; };
use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView}; use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView};
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
@ -19,17 +19,27 @@ use ts_rs::TS;
pub struct CreatePost { pub struct CreatePost {
pub name: String, pub name: String,
pub community_id: CommunityId, pub community_id: CommunityId,
#[cfg_attr(feature = "full", ts(optional))]
pub url: Option<String>, pub url: Option<String>,
/// An optional body for the post in markdown. /// An optional body for the post in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub body: Option<String>, pub body: Option<String>,
/// An optional alt_text, usable for image posts. /// An optional alt_text, usable for image posts.
#[cfg_attr(feature = "full", ts(optional))]
pub alt_text: Option<String>, pub alt_text: Option<String>,
/// A honeypot to catch bots. Should be None. /// A honeypot to catch bots. Should be None.
#[cfg_attr(feature = "full", ts(optional))]
pub honeypot: Option<String>, pub honeypot: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub language_id: Option<LanguageId>, pub language_id: Option<LanguageId>,
/// Instead of fetching a thumbnail, use a custom one. /// Instead of fetching a thumbnail, use a custom one.
#[cfg_attr(feature = "full", ts(optional))]
pub custom_thumbnail: Option<String>, pub custom_thumbnail: Option<String>,
/// Time when this post should be scheduled. Null means publish immediately.
#[cfg_attr(feature = "full", ts(optional))]
pub scheduled_publish_time: Option<i64>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -43,9 +53,12 @@ pub struct PostResponse {
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
// TODO this should be made into a tagged enum
/// Get a post. Needs either the post id, or comment_id. /// Get a post. Needs either the post id, or comment_id.
pub struct GetPost { pub struct GetPost {
#[cfg_attr(feature = "full", ts(optional))]
pub id: Option<PostId>, pub id: Option<PostId>,
#[cfg_attr(feature = "full", ts(optional))]
pub comment_id: Option<CommentId>, pub comment_id: Option<CommentId>,
} }
@ -68,21 +81,37 @@ pub struct GetPostResponse {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Get a list of posts. /// Get a list of posts.
pub struct GetPosts { pub struct GetPosts {
#[cfg_attr(feature = "full", ts(optional))]
pub type_: Option<ListingType>, pub type_: Option<ListingType>,
pub sort: Option<SortType>, #[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<PostSortType>,
/// DEPRECATED, use page_cursor /// DEPRECATED, use page_cursor
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_name: Option<String>, pub community_name: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub saved_only: Option<bool>, pub saved_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub liked_only: Option<bool>, pub liked_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub disliked_only: Option<bool>, pub disliked_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub show_hidden: Option<bool>, pub show_hidden: Option<bool>,
/// If true, then show the read posts (even if your user setting is to hide them) /// If true, then show the read posts (even if your user setting is to hide them)
#[cfg_attr(feature = "full", ts(optional))]
pub show_read: Option<bool>, pub show_read: Option<bool>,
/// If true, then show the nsfw posts (even if your user setting is to hide them) /// If true, then show the nsfw posts (even if your user setting is to hide them)
#[cfg_attr(feature = "full", ts(optional))]
pub show_nsfw: Option<bool>, pub show_nsfw: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
/// If true, then only show posts with no comments
pub no_comments_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_cursor: Option<PaginationCursor>, pub page_cursor: Option<PaginationCursor>,
} }
@ -94,6 +123,7 @@ pub struct GetPosts {
pub struct GetPostsResponse { pub struct GetPostsResponse {
pub posts: Vec<PostView>, pub posts: Vec<PostView>,
/// the pagination cursor to use to fetch the next page /// the pagination cursor to use to fetch the next page
#[cfg_attr(feature = "full", ts(optional))]
pub next_page: Option<PaginationCursor>, pub next_page: Option<PaginationCursor>,
} }
@ -114,16 +144,26 @@ pub struct CreatePostLike {
/// Edit a post. /// Edit a post.
pub struct EditPost { pub struct EditPost {
pub post_id: PostId, pub post_id: PostId,
#[cfg_attr(feature = "full", ts(optional))]
pub name: Option<String>, pub name: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub url: Option<String>, pub url: Option<String>,
/// An optional body for the post in markdown. /// An optional body for the post in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub body: Option<String>, pub body: Option<String>,
/// An optional alt_text, usable for image posts. /// An optional alt_text, usable for image posts.
#[cfg_attr(feature = "full", ts(optional))]
pub alt_text: Option<String>, pub alt_text: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub language_id: Option<LanguageId>, pub language_id: Option<LanguageId>,
/// Instead of fetching a thumbnail, use a custom one. /// Instead of fetching a thumbnail, use a custom one.
#[cfg_attr(feature = "full", ts(optional))]
pub custom_thumbnail: Option<String>, pub custom_thumbnail: Option<String>,
/// Time when this post should be scheduled. Null means publish immediately.
#[cfg_attr(feature = "full", ts(optional))]
pub scheduled_publish_time: Option<i64>,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
@ -143,6 +183,7 @@ pub struct DeletePost {
pub struct RemovePost { pub struct RemovePost {
pub post_id: PostId, pub post_id: PostId,
pub removed: bool, pub removed: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
} }
@ -226,12 +267,18 @@ pub struct ResolvePostReport {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// List post reports. /// List post reports.
pub struct ListPostReports { pub struct ListPostReports {
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
/// Only shows the unresolved reports /// Only shows the unresolved reports
#[cfg_attr(feature = "full", ts(optional))]
pub unresolved_only: Option<bool>, pub unresolved_only: Option<bool>,
// TODO make into tagged enum at some point
/// if no community is given, it returns reports for all communities moderated by the auth user /// if no community is given, it returns reports for all communities moderated by the auth user
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_id: Option<PostId>, pub post_id: Option<PostId>,
} }
@ -267,6 +314,7 @@ pub struct GetSiteMetadataResponse {
pub struct LinkMetadata { pub struct LinkMetadata {
#[serde(flatten)] #[serde(flatten)]
pub opengraph_data: OpenGraphData, pub opengraph_data: OpenGraphData,
#[cfg_attr(feature = "full", ts(optional))]
pub content_type: Option<String>, pub content_type: Option<String>,
} }
@ -276,9 +324,13 @@ pub struct LinkMetadata {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Site metadata, from its opengraph tags. /// Site metadata, from its opengraph tags.
pub struct OpenGraphData { pub struct OpenGraphData {
#[cfg_attr(feature = "full", ts(optional))]
pub title: Option<String>, pub title: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub description: Option<String>, pub description: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub(crate) image: Option<DbUrl>, pub(crate) image: Option<DbUrl>,
#[cfg_attr(feature = "full", ts(optional))]
pub embed_video_url: Option<DbUrl>, pub embed_video_url: Option<DbUrl>,
} }
@ -289,7 +341,9 @@ pub struct OpenGraphData {
/// List post likes. Admins-only. /// List post likes. Admins-only.
pub struct ListPostLikes { pub struct ListPostLikes {
pub post_id: PostId, pub post_id: PostId,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
} }

View file

@ -47,9 +47,13 @@ pub struct MarkPrivateMessageAsRead {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Get your private messages. /// Get your private messages.
pub struct GetPrivateMessages { pub struct GetPrivateMessages {
#[cfg_attr(feature = "full", ts(optional))]
pub unread_only: Option<bool>, pub unread_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub creator_id: Option<PersonId>, pub creator_id: Option<PersonId>,
} }
@ -102,9 +106,12 @@ pub struct ResolvePrivateMessageReport {
/// List private message reports. /// List private message reports.
// TODO , perhaps GetReports should be a tagged enum list too. // TODO , perhaps GetReports should be a tagged enum list too.
pub struct ListPrivateMessageReports { pub struct ListPrivateMessageReports {
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
/// Only shows the unresolved reports /// Only shows the unresolved reports
#[cfg_attr(feature = "full", ts(optional))]
pub unresolved_only: Option<bool>, pub unresolved_only: Option<bool>,
} }

View file

@ -3,7 +3,7 @@ use crate::{
lemmy_db_schema::traits::Crud, lemmy_db_schema::traits::Crud,
post::{LinkMetadata, OpenGraphData}, post::{LinkMetadata, OpenGraphData},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{local_site_opt_to_sensitive, proxy_image_link}, utils::proxy_image_link,
}; };
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@ -13,8 +13,8 @@ use lemmy_db_schema::{
newtypes::DbUrl, newtypes::DbUrl,
source::{ source::{
images::{ImageDetailsForm, LocalImage, LocalImageForm}, images::{ImageDetailsForm, LocalImage, LocalImageForm},
local_site::LocalSite,
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
site::Site,
}, },
}; };
use lemmy_utils::{ use lemmy_utils::{
@ -44,6 +44,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder {
.user_agent(user_agent.clone()) .user_agent(user_agent.clone())
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.connect_timeout(REQWEST_TIMEOUT) .connect_timeout(REQWEST_TIMEOUT)
.use_rustls_tls()
} }
/// Fetches metadata for the given link and optionally generates thumbnail. /// Fetches metadata for the given link and optionally generates thumbnail.
@ -130,7 +131,6 @@ pub async fn generate_post_link_metadata(
post: Post, post: Post,
custom_thumbnail: Option<Url>, custom_thumbnail: Option<Url>,
send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static, send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static,
local_site: Option<LocalSite>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let metadata = match &post.url { let metadata = match &post.url {
@ -144,7 +144,8 @@ pub async fn generate_post_link_metadata(
.is_some_and(|content_type| content_type.starts_with("image")); .is_some_and(|content_type| content_type.starts_with("image"));
// Decide if we are allowed to generate local thumbnail // Decide if we are allowed to generate local thumbnail
let allow_sensitive = local_site_opt_to_sensitive(&local_site); let site = Site::read_local(&mut context.pool()).await?;
let allow_sensitive = site.content_warning.is_some();
let allow_generate_thumbnail = allow_sensitive || !post.nsfw; let allow_generate_thumbnail = allow_sensitive || !post.nsfw;
let image_url = if is_image_post { let image_url = if is_image_post {
@ -174,7 +175,7 @@ pub async fn generate_post_link_metadata(
}; };
let updated_post = Post::update(&mut context.pool(), post.id, &form).await?; let updated_post = Post::update(&mut context.pool(), post.id, &form).await?;
if let Some(send_activity) = send_activity(updated_post) { if let Some(send_activity) = send_activity(updated_post) {
ActivityChannel::submit_activity(send_activity, &context).await?; ActivityChannel::submit_activity(send_activity, &context)?;
} }
Ok(()) Ok(())
} }
@ -353,9 +354,10 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
// fetch remote non-pictrs images for persistent thumbnail link // fetch remote non-pictrs images for persistent thumbnail link
// TODO: should limit size once supported by pictrs // TODO: should limit size once supported by pictrs
let fetch_url = format!( let fetch_url = format!(
"{}image/download?url={}", "{}image/download?url={}&resize={}",
pictrs_config.url, pictrs_config.url,
encode(image_url.as_str()) encode(image_url.as_str()),
context.settings().pictrs_config()?.max_thumbnail_size
); );
let res = context let res = context
@ -470,14 +472,13 @@ pub async fn replace_image(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{
context::LemmyContext, context::LemmyContext,
request::{extract_opengraph_data, fetch_link_metadata}, request::{extract_opengraph_data, fetch_link_metadata},
}; };
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serial_test::serial; use serial_test::serial;
use url::Url; use url::Url;
@ -485,10 +486,10 @@ mod tests {
// These helped with testing // These helped with testing
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_link_metadata() { async fn test_link_metadata() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap(); let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ")?;
let sample_res = fetch_link_metadata(&sample_url, &context).await.unwrap(); let sample_res = fetch_link_metadata(&sample_url, &context).await?;
assert_eq!( assert_eq!(
Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()), Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()),
sample_res.opengraph_data.title sample_res.opengraph_data.title
@ -499,8 +500,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
Some( Some(
Url::parse("https://gitlab.com/uploads/-/system/project/avatar/4877469/iod_logo.png") Url::parse("https://gitlab.com/uploads/-/system/project/avatar/4877469/iod_logo.png")?
.unwrap()
.into() .into()
), ),
sample_res.opengraph_data.image sample_res.opengraph_data.image
@ -510,19 +510,21 @@ mod tests {
Some(mime::TEXT_HTML_UTF_8.to_string()), Some(mime::TEXT_HTML_UTF_8.to_string()),
sample_res.content_type sample_res.content_type
); );
Ok(())
} }
#[test] #[test]
fn test_resolve_image_url() { fn test_resolve_image_url() -> LemmyResult<()> {
// url that lists the opengraph fields // url that lists the opengraph fields
let url = Url::parse("https://example.com/one/two.html").unwrap(); let url = Url::parse("https://example.com/one/two.html")?;
// root relative url // root relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='/image.jpg'></head><body></body></html>"; let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='/image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
assert_eq!( assert_eq!(
metadata.image, metadata.image,
Some(Url::parse("https://example.com/image.jpg").unwrap().into()) Some(Url::parse("https://example.com/image.jpg")?.into())
); );
// base relative url // base relative url
@ -530,11 +532,7 @@ mod tests {
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
assert_eq!( assert_eq!(
metadata.image, metadata.image,
Some( Some(Url::parse("https://example.com/one/image.jpg")?.into())
Url::parse("https://example.com/one/image.jpg")
.unwrap()
.into()
)
); );
// absolute url // absolute url
@ -542,7 +540,7 @@ mod tests {
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
assert_eq!( assert_eq!(
metadata.image, metadata.image,
Some(Url::parse("https://cdn.host.com/image.jpg").unwrap().into()) Some(Url::parse("https://cdn.host.com/image.jpg")?.into())
); );
// protocol relative url // protocol relative url
@ -550,7 +548,9 @@ mod tests {
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata"); let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
assert_eq!( assert_eq!(
metadata.image, metadata.image,
Some(Url::parse("https://example.com/image.jpg").unwrap().into()) Some(Url::parse("https://example.com/image.jpg")?.into())
); );
Ok(())
} }
} }

View file

@ -59,6 +59,8 @@ pub enum SendActivityData {
score: i16, score: i16,
}, },
FollowCommunity(Community, Person, bool), FollowCommunity(Community, Person, bool),
AcceptFollower(CommunityId, PersonId),
RejectFollower(CommunityId, PersonId),
UpdateCommunity(Person, Community), UpdateCommunity(Person, Community),
DeleteCommunity(Person, Community, bool), DeleteCommunity(Person, Community, bool),
RemoveCommunity { RemoveCommunity {
@ -83,7 +85,7 @@ pub enum SendActivityData {
moderator: Person, moderator: Person,
banned_user: Person, banned_user: Person,
reason: Option<String>, reason: Option<String>,
remove_data: Option<bool>, remove_or_restore_data: Option<bool>,
ban: bool, ban: bool,
expires: Option<i64>, expires: Option<i64>,
}, },
@ -123,10 +125,7 @@ impl ActivityChannel {
lock.recv().await lock.recv().await
} }
pub async fn submit_activity( pub fn submit_activity(data: SendActivityData, _context: &Data<LemmyContext>) -> LemmyResult<()> {
data: SendActivityData,
_context: &Data<LemmyContext>,
) -> LemmyResult<()> {
// could do `ACTIVITY_CHANNEL.keepalive_sender.lock()` instead and get rid of weak_sender, // could do `ACTIVITY_CHANNEL.keepalive_sender.lock()` instead and get rid of weak_sender,
// not sure which way is more efficient // not sure which way is more efficient
if let Some(sender) = ACTIVITY_CHANNEL.weak_sender.upgrade() { if let Some(sender) = ACTIVITY_CHANNEL.weak_sender.upgrade() {

View file

@ -11,34 +11,35 @@ use lemmy_db_schema::{
RegistrationApplicationId, RegistrationApplicationId,
}, },
source::{ source::{
community::Community,
federation_queue_state::FederationQueueState, federation_queue_state::FederationQueueState,
instance::Instance, instance::Instance,
language::Language, language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
oauth_provider::{OAuthProvider, PublicOAuthProvider},
person::Person,
tagline::Tagline, tagline::Tagline,
}, },
CommentSortType,
FederationMode,
ListingType, ListingType,
ModlogActionType, ModlogActionType,
PostListingMode, PostListingMode,
PostSortType,
RegistrationMode, RegistrationMode,
SearchType, SearchType,
SortType,
}; };
use lemmy_db_views::structs::{ use lemmy_db_views::structs::{
CommentView, CommentView,
CustomEmojiView,
LocalUserView, LocalUserView,
PostView, PostView,
RegistrationApplicationView, RegistrationApplicationView,
SiteView, SiteView,
}; };
use lemmy_db_views_actor::structs::{ use lemmy_db_views_actor::structs::{
CommunityBlockView,
CommunityFollowerView, CommunityFollowerView,
CommunityModeratorView, CommunityModeratorView,
CommunityView, CommunityView,
InstanceBlockView,
PersonBlockView,
PersonView, PersonView,
}; };
use lemmy_db_views_moderator::structs::{ use lemmy_db_views_moderator::structs::{
@ -70,14 +71,32 @@ use ts_rs::TS;
/// Searches the site, given a query string, and some optional filters. /// Searches the site, given a query string, and some optional filters.
pub struct Search { pub struct Search {
pub q: String, pub q: String,
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_name: Option<String>, pub community_name: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub creator_id: Option<PersonId>, pub creator_id: Option<PersonId>,
#[cfg_attr(feature = "full", ts(optional))]
pub type_: Option<SearchType>, pub type_: Option<SearchType>,
pub sort: Option<SortType>, #[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<PostSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub listing_type: Option<ListingType>, pub listing_type: Option<ListingType>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub title_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_url_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub saved_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub liked_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub disliked_only: Option<bool>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -109,9 +128,13 @@ pub struct ResolveObject {
// TODO Change this to an enum // TODO Change this to an enum
/// The response of an apub object fetch. /// The response of an apub object fetch.
pub struct ResolveObjectResponse { pub struct ResolveObjectResponse {
#[cfg_attr(feature = "full", ts(optional))]
pub comment: Option<CommentView>, pub comment: Option<CommentView>,
#[cfg_attr(feature = "full", ts(optional))]
pub post: Option<PostView>, pub post: Option<PostView>,
#[cfg_attr(feature = "full", ts(optional))]
pub community: Option<CommunityView>, pub community: Option<CommunityView>,
#[cfg_attr(feature = "full", ts(optional))]
pub person: Option<PersonView>, pub person: Option<PersonView>,
} }
@ -121,13 +144,21 @@ pub struct ResolveObjectResponse {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Fetches the modlog. /// Fetches the modlog.
pub struct GetModlog { pub struct GetModlog {
#[cfg_attr(feature = "full", ts(optional))]
pub mod_person_id: Option<PersonId>, pub mod_person_id: Option<PersonId>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub type_: Option<ModlogActionType>, pub type_: Option<ModlogActionType>,
#[cfg_attr(feature = "full", ts(optional))]
pub other_person_id: Option<PersonId>, pub other_person_id: Option<PersonId>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_id: Option<PostId>, pub post_id: Option<PostId>,
#[cfg_attr(feature = "full", ts(optional))]
pub comment_id: Option<CommentId>, pub comment_id: Option<CommentId>,
} }
@ -161,47 +192,96 @@ pub struct GetModlogResponse {
/// Creates a site. Should be done after first running lemmy. /// Creates a site. Should be done after first running lemmy.
pub struct CreateSite { pub struct CreateSite {
pub name: String, pub name: String,
#[cfg_attr(feature = "full", ts(optional))]
pub sidebar: Option<String>, pub sidebar: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub description: Option<String>, pub description: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub icon: Option<String>, pub icon: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub banner: Option<String>, pub banner: Option<String>,
pub enable_downvotes: Option<bool>, #[cfg_attr(feature = "full", ts(optional))]
pub enable_nsfw: Option<bool>, pub enable_nsfw: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub require_email_verification: Option<bool>, pub require_email_verification: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub application_question: Option<String>, pub application_question: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub private_instance: Option<bool>, pub private_instance: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub default_theme: Option<String>, pub default_theme: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_listing_type: Option<ListingType>, pub default_post_listing_type: Option<ListingType>,
pub default_sort_type: Option<SortType>, #[cfg_attr(feature = "full", ts(optional))]
pub legal_information: Option<String>,
pub application_email_admins: Option<bool>,
pub hide_modlog_mod_names: Option<bool>,
pub discussion_languages: Option<Vec<LanguageId>>,
pub slur_filter_regex: Option<String>,
pub actor_name_max_length: Option<i32>,
pub rate_limit_message: Option<i32>,
pub rate_limit_message_per_second: Option<i32>,
pub rate_limit_post: Option<i32>,
pub rate_limit_post_per_second: Option<i32>,
pub rate_limit_register: Option<i32>,
pub rate_limit_register_per_second: Option<i32>,
pub rate_limit_image: Option<i32>,
pub rate_limit_image_per_second: Option<i32>,
pub rate_limit_comment: Option<i32>,
pub rate_limit_comment_per_second: Option<i32>,
pub rate_limit_search: Option<i32>,
pub rate_limit_search_per_second: Option<i32>,
pub federation_enabled: Option<bool>,
pub federation_debug: Option<bool>,
pub captcha_enabled: Option<bool>,
pub captcha_difficulty: Option<String>,
pub allowed_instances: Option<Vec<String>>,
pub blocked_instances: Option<Vec<String>>,
pub taglines: Option<Vec<String>>,
pub registration_mode: Option<RegistrationMode>,
pub content_warning: Option<String>,
pub default_post_listing_mode: Option<PostListingMode>, pub default_post_listing_mode: Option<PostListingMode>,
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_sort_type: Option<PostSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub default_comment_sort_type: Option<CommentSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub legal_information: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub application_email_admins: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub hide_modlog_mod_names: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub discussion_languages: Option<Vec<LanguageId>>,
#[cfg_attr(feature = "full", ts(optional))]
pub slur_filter_regex: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub actor_name_max_length: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_message: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_message_per_second: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_post: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_post_per_second: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_register: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_register_per_second: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_image: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_image_per_second: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_comment: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_comment_per_second: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_search: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_search_per_second: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub federation_enabled: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub federation_debug: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub captcha_enabled: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub captcha_difficulty: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub allowed_instances: Option<Vec<String>>,
#[cfg_attr(feature = "full", ts(optional))]
pub blocked_instances: Option<Vec<String>>,
#[cfg_attr(feature = "full", ts(optional))]
pub registration_mode: Option<RegistrationMode>,
#[cfg_attr(feature = "full", ts(optional))]
pub oauth_registration: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub content_warning: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_upvotes: Option<FederationMode>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_downvotes: Option<FederationMode>,
#[cfg_attr(feature = "full", ts(optional))]
pub comment_upvotes: Option<FederationMode>,
#[cfg_attr(feature = "full", ts(optional))]
pub comment_downvotes: Option<FederationMode>,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -210,85 +290,143 @@ pub struct CreateSite {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Edits a site. /// Edits a site.
pub struct EditSite { pub struct EditSite {
#[cfg_attr(feature = "full", ts(optional))]
pub name: Option<String>, pub name: Option<String>,
/// A sidebar for the site, in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub sidebar: Option<String>, pub sidebar: Option<String>,
/// A shorter, one line description of your site. /// A shorter, one line description of your site.
#[cfg_attr(feature = "full", ts(optional))]
pub description: Option<String>, pub description: Option<String>,
/// A url for your site's icon. /// A url for your site's icon.
#[cfg_attr(feature = "full", ts(optional))]
pub icon: Option<String>, pub icon: Option<String>,
/// A url for your site's banner. /// A url for your site's banner.
#[cfg_attr(feature = "full", ts(optional))]
pub banner: Option<String>, pub banner: Option<String>,
/// Whether to enable downvotes.
pub enable_downvotes: Option<bool>,
/// Whether to enable NSFW. /// Whether to enable NSFW.
#[cfg_attr(feature = "full", ts(optional))]
pub enable_nsfw: Option<bool>, pub enable_nsfw: Option<bool>,
/// Limits community creation to admins only. /// Limits community creation to admins only.
#[cfg_attr(feature = "full", ts(optional))]
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
/// Whether to require email verification. /// Whether to require email verification.
#[cfg_attr(feature = "full", ts(optional))]
pub require_email_verification: Option<bool>, pub require_email_verification: Option<bool>,
/// Your application question form. This is in markdown, and can be many questions. /// Your application question form. This is in markdown, and can be many questions.
#[cfg_attr(feature = "full", ts(optional))]
pub application_question: Option<String>, pub application_question: Option<String>,
/// Whether your instance is public, or private. /// Whether your instance is public, or private.
#[cfg_attr(feature = "full", ts(optional))]
pub private_instance: Option<bool>, pub private_instance: Option<bool>,
/// The default theme. Usually "browser" /// The default theme. Usually "browser"
#[cfg_attr(feature = "full", ts(optional))]
pub default_theme: Option<String>, pub default_theme: Option<String>,
/// The default post listing type, usually "local"
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_listing_type: Option<ListingType>, pub default_post_listing_type: Option<ListingType>,
/// The default sort, usually "active" /// Default value for listing mode, usually "list"
pub default_sort_type: Option<SortType>, #[cfg_attr(feature = "full", ts(optional))]
pub default_post_listing_mode: Option<PostListingMode>,
/// The default post sort, usually "active"
#[cfg_attr(feature = "full", ts(optional))]
pub default_post_sort_type: Option<PostSortType>,
/// The default comment sort, usually "hot"
#[cfg_attr(feature = "full", ts(optional))]
pub default_comment_sort_type: Option<CommentSortType>,
/// An optional page of legal information /// An optional page of legal information
#[cfg_attr(feature = "full", ts(optional))]
pub legal_information: Option<String>, pub legal_information: Option<String>,
/// Whether to email admins when receiving a new application. /// Whether to email admins when receiving a new application.
#[cfg_attr(feature = "full", ts(optional))]
pub application_email_admins: Option<bool>, pub application_email_admins: Option<bool>,
/// Whether to hide moderator names from the modlog. /// Whether to hide moderator names from the modlog.
#[cfg_attr(feature = "full", ts(optional))]
pub hide_modlog_mod_names: Option<bool>, pub hide_modlog_mod_names: Option<bool>,
/// A list of allowed discussion languages. /// A list of allowed discussion languages.
#[cfg_attr(feature = "full", ts(optional))]
pub discussion_languages: Option<Vec<LanguageId>>, pub discussion_languages: Option<Vec<LanguageId>>,
/// A regex string of items to filter. /// A regex string of items to filter.
#[cfg_attr(feature = "full", ts(optional))]
pub slur_filter_regex: Option<String>, pub slur_filter_regex: Option<String>,
/// The max length of actor names. /// The max length of actor names.
#[cfg_attr(feature = "full", ts(optional))]
pub actor_name_max_length: Option<i32>, pub actor_name_max_length: Option<i32>,
/// The number of messages allowed in a given time frame. /// The number of messages allowed in a given time frame.
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_message: Option<i32>, pub rate_limit_message: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_message_per_second: Option<i32>, pub rate_limit_message_per_second: Option<i32>,
/// The number of posts allowed in a given time frame. /// The number of posts allowed in a given time frame.
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_post: Option<i32>, pub rate_limit_post: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_post_per_second: Option<i32>, pub rate_limit_post_per_second: Option<i32>,
/// The number of registrations allowed in a given time frame. /// The number of registrations allowed in a given time frame.
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_register: Option<i32>, pub rate_limit_register: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_register_per_second: Option<i32>, pub rate_limit_register_per_second: Option<i32>,
/// The number of image uploads allowed in a given time frame. /// The number of image uploads allowed in a given time frame.
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_image: Option<i32>, pub rate_limit_image: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_image_per_second: Option<i32>, pub rate_limit_image_per_second: Option<i32>,
/// The number of comments allowed in a given time frame. /// The number of comments allowed in a given time frame.
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_comment: Option<i32>, pub rate_limit_comment: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_comment_per_second: Option<i32>, pub rate_limit_comment_per_second: Option<i32>,
/// The number of searches allowed in a given time frame. /// The number of searches allowed in a given time frame.
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_search: Option<i32>, pub rate_limit_search: Option<i32>,
#[cfg_attr(feature = "full", ts(optional))]
pub rate_limit_search_per_second: Option<i32>, pub rate_limit_search_per_second: Option<i32>,
/// Whether to enable federation. /// Whether to enable federation.
#[cfg_attr(feature = "full", ts(optional))]
pub federation_enabled: Option<bool>, pub federation_enabled: Option<bool>,
/// Enables federation debugging. /// Enables federation debugging.
#[cfg_attr(feature = "full", ts(optional))]
pub federation_debug: Option<bool>, pub federation_debug: Option<bool>,
/// Whether to enable captchas for signups. /// Whether to enable captchas for signups.
#[cfg_attr(feature = "full", ts(optional))]
pub captcha_enabled: Option<bool>, pub captcha_enabled: Option<bool>,
/// The captcha difficulty. Can be easy, medium, or hard /// The captcha difficulty. Can be easy, medium, or hard
#[cfg_attr(feature = "full", ts(optional))]
pub captcha_difficulty: Option<String>, pub captcha_difficulty: Option<String>,
/// A list of allowed instances. If none are set, federation is open. /// A list of allowed instances. If none are set, federation is open.
#[cfg_attr(feature = "full", ts(optional))]
pub allowed_instances: Option<Vec<String>>, pub allowed_instances: Option<Vec<String>>,
/// A list of blocked instances. /// A list of blocked instances.
#[cfg_attr(feature = "full", ts(optional))]
pub blocked_instances: Option<Vec<String>>, pub blocked_instances: Option<Vec<String>>,
/// A list of blocked URLs /// A list of blocked URLs
#[cfg_attr(feature = "full", ts(optional))]
pub blocked_urls: Option<Vec<String>>, pub blocked_urls: Option<Vec<String>>,
/// A list of taglines shown at the top of the front page. #[cfg_attr(feature = "full", ts(optional))]
pub taglines: Option<Vec<String>>,
pub registration_mode: Option<RegistrationMode>, pub registration_mode: Option<RegistrationMode>,
/// Whether to email admins for new reports. /// Whether to email admins for new reports.
#[cfg_attr(feature = "full", ts(optional))]
pub reports_email_admins: Option<bool>, pub reports_email_admins: Option<bool>,
/// If present, nsfw content is visible by default. Should be displayed by frontends/clients /// If present, nsfw content is visible by default. Should be displayed by frontends/clients
/// when the site is first opened by a user. /// when the site is first opened by a user.
#[cfg_attr(feature = "full", ts(optional))]
pub content_warning: Option<String>, pub content_warning: Option<String>,
/// Default value for [LocalUser.post_listing_mode] /// Whether or not external auth methods can auto-register users.
pub default_post_listing_mode: Option<PostListingMode>, #[cfg_attr(feature = "full", ts(optional))]
pub oauth_registration: Option<bool>,
/// What kind of post upvotes your site allows.
#[cfg_attr(feature = "full", ts(optional))]
pub post_upvotes: Option<FederationMode>,
/// What kind of post downvotes your site allows.
#[cfg_attr(feature = "full", ts(optional))]
pub post_downvotes: Option<FederationMode>,
/// What kind of comment upvotes your site allows.
#[cfg_attr(feature = "full", ts(optional))]
pub comment_upvotes: Option<FederationMode>,
/// What kind of comment downvotes your site allows.
#[cfg_attr(feature = "full", ts(optional))]
pub comment_downvotes: Option<FederationMode>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -297,7 +435,8 @@ pub struct EditSite {
/// The response for a site. /// The response for a site.
pub struct SiteResponse { pub struct SiteResponse {
pub site_view: SiteView, pub site_view: SiteView,
pub taglines: Vec<Tagline>, /// deprecated, use field `tagline` or /api/v3/tagline/list
pub taglines: Vec<()>,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -309,13 +448,22 @@ pub struct GetSiteResponse {
pub site_view: SiteView, pub site_view: SiteView,
pub admins: Vec<PersonView>, pub admins: Vec<PersonView>,
pub version: String, pub version: String,
#[cfg_attr(feature = "full", ts(optional))]
pub my_user: Option<MyUserInfo>, pub my_user: Option<MyUserInfo>,
pub all_languages: Vec<Language>, pub all_languages: Vec<Language>,
pub discussion_languages: Vec<LanguageId>, pub discussion_languages: Vec<LanguageId>,
/// A list of taglines shown at the top of the front page. /// deprecated, use field `tagline` or /api/v3/tagline/list
pub taglines: Vec<Tagline>, pub taglines: Vec<()>,
/// A list of custom emojis your site supports. /// deprecated, use /api/v3/custom_emoji/list
pub custom_emojis: Vec<CustomEmojiView>, pub custom_emojis: Vec<()>,
/// If the site has any taglines, a random one is included here for displaying
#[cfg_attr(feature = "full", ts(optional))]
pub tagline: Option<Tagline>,
/// A list of external auth methods your site supports.
#[cfg_attr(feature = "full", ts(optional))]
pub oauth_providers: Option<Vec<PublicOAuthProvider>>,
#[cfg_attr(feature = "full", ts(optional))]
pub admin_oauth_providers: Option<Vec<OAuthProvider>>,
pub blocked_urls: Vec<LocalSiteUrlBlocklist>, pub blocked_urls: Vec<LocalSiteUrlBlocklist>,
} }
@ -326,6 +474,7 @@ pub struct GetSiteResponse {
/// A response of federated instances. /// A response of federated instances.
pub struct GetFederatedInstancesResponse { pub struct GetFederatedInstancesResponse {
/// Optional, because federation may be disabled. /// Optional, because federation may be disabled.
#[cfg_attr(feature = "full", ts(optional))]
pub federated_instances: Option<FederatedInstances>, pub federated_instances: Option<FederatedInstances>,
} }
@ -337,9 +486,9 @@ pub struct MyUserInfo {
pub local_user_view: LocalUserView, pub local_user_view: LocalUserView,
pub follows: Vec<CommunityFollowerView>, pub follows: Vec<CommunityFollowerView>,
pub moderates: Vec<CommunityModeratorView>, pub moderates: Vec<CommunityModeratorView>,
pub community_blocks: Vec<CommunityBlockView>, pub community_blocks: Vec<Community>,
pub instance_blocks: Vec<InstanceBlockView>, pub instance_blocks: Vec<Instance>,
pub person_blocks: Vec<PersonBlockView>, pub person_blocks: Vec<Person>,
pub discussion_languages: Vec<LanguageId>, pub discussion_languages: Vec<LanguageId>,
} }
@ -361,6 +510,7 @@ pub struct ReadableFederationState {
#[serde(flatten)] #[serde(flatten)]
internal_state: FederationQueueState, internal_state: FederationQueueState,
/// timestamp of the next retry attempt (null if fail count is 0) /// timestamp of the next retry attempt (null if fail count is 0)
#[cfg_attr(feature = "full", ts(optional))]
next_retry: Option<DateTime<Utc>>, next_retry: Option<DateTime<Utc>>,
} }
@ -385,6 +535,7 @@ pub struct InstanceWithFederationState {
pub instance: Instance, pub instance: Instance,
/// if federation to this instance is or was active, show state of outgoing federation to this /// if federation to this instance is or was active, show state of outgoing federation to this
/// instance /// instance
#[cfg_attr(feature = "full", ts(optional))]
pub federation_state: Option<ReadableFederationState>, pub federation_state: Option<ReadableFederationState>,
} }
@ -395,6 +546,7 @@ pub struct InstanceWithFederationState {
/// Purges a person from the database. This will delete all content attached to that person. /// Purges a person from the database. This will delete all content attached to that person.
pub struct PurgePerson { pub struct PurgePerson {
pub person_id: PersonId, pub person_id: PersonId,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
} }
@ -405,6 +557,7 @@ pub struct PurgePerson {
/// Purges a community from the database. This will delete all content attached to that community. /// Purges a community from the database. This will delete all content attached to that community.
pub struct PurgeCommunity { pub struct PurgeCommunity {
pub community_id: CommunityId, pub community_id: CommunityId,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
} }
@ -415,6 +568,7 @@ pub struct PurgeCommunity {
/// Purges a post from the database. This will delete all content attached to that post. /// Purges a post from the database. This will delete all content attached to that post.
pub struct PurgePost { pub struct PurgePost {
pub post_id: PostId, pub post_id: PostId,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
} }
@ -425,6 +579,7 @@ pub struct PurgePost {
/// Purges a comment from the database. This will delete all content attached to that comment. /// Purges a comment from the database. This will delete all content attached to that comment.
pub struct PurgeComment { pub struct PurgeComment {
pub comment_id: CommentId, pub comment_id: CommentId,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>, pub reason: Option<String>,
} }
@ -435,8 +590,11 @@ pub struct PurgeComment {
/// Fetches a list of registration applications. /// Fetches a list of registration applications.
pub struct ListRegistrationApplications { pub struct ListRegistrationApplications {
/// Only shows the unread applications (IE those without an admin actor) /// Only shows the unread applications (IE those without an admin actor)
#[cfg_attr(feature = "full", ts(optional))]
pub unread_only: Option<bool>, pub unread_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>, pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>, pub limit: Option<i64>,
} }
@ -465,6 +623,7 @@ pub struct GetRegistrationApplication {
pub struct ApproveRegistrationApplication { pub struct ApproveRegistrationApplication {
pub id: RegistrationApplicationId, pub id: RegistrationApplicationId,
pub approve: bool, pub approve: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub deny_reason: Option<String>, pub deny_reason: Option<String>,
} }

View file

@ -0,0 +1,57 @@
use lemmy_db_schema::{newtypes::TaglineId, source::tagline::Tagline};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create a tagline
pub struct CreateTagline {
pub content: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Update a tagline
pub struct UpdateTagline {
pub id: TaglineId,
pub content: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete a tagline
pub struct DeleteTagline {
pub id: TaglineId,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct TaglineResponse {
pub tagline: Tagline,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A response for taglines.
pub struct ListTaglinesResponse {
pub taglines: Vec<Tagline>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetches a list of taglines.
pub struct ListTaglines {
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>,
}

View file

@ -11,32 +11,38 @@ use chrono::{DateTime, Days, Local, TimeZone, Utc};
use enum_map::{enum_map, EnumMap}; use enum_map::{enum_map, EnumMap};
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId}, newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId},
source::{ source::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentLike, CommentUpdateForm},
community::{Community, CommunityModerator, CommunityUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm},
community_block::CommunityBlock, community_block::CommunityBlock,
email_verification::{EmailVerification, EmailVerificationForm}, email_verification::{EmailVerification, EmailVerificationForm},
images::RemoteImage, images::{ImageDetails, RemoteImage},
instance::Instance, instance::Instance,
instance_block::InstanceBlock, instance_block::InstanceBlock,
local_site::LocalSite, local_site::LocalSite,
local_site_rate_limit::LocalSiteRateLimit, local_site_rate_limit::LocalSiteRateLimit,
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm},
oauth_account::OAuthAccount,
password_reset_request::PasswordResetRequest, password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
person_block::PersonBlock, person_block::PersonBlock,
post::{Post, PostRead}, post::{Post, PostLike, PostRead},
registration_application::RegistrationApplication,
site::Site, site::Site,
}, },
traits::Crud, traits::{Crud, Likeable},
utils::DbPool, utils::DbPool,
FederationMode,
RegistrationMode,
}; };
use lemmy_db_views::{ use lemmy_db_views::{
comment_view::CommentQuery, comment_view::CommentQuery,
structs::{LocalImageView, LocalUserView}, structs::{LocalImageView, LocalUserView, SiteView},
}; };
use lemmy_db_views_actor::structs::{ use lemmy_db_views_actor::structs::{
CommunityFollowerView,
CommunityModeratorView, CommunityModeratorView,
CommunityPersonBanView, CommunityPersonBanView,
CommunityView, CommunityView,
@ -45,10 +51,14 @@ use lemmy_utils::{
email::{send_email, translations::Lang}, email::{send_email, translations::Lang},
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
rate_limit::{ActionType, BucketConfig}, rate_limit::{ActionType, BucketConfig},
settings::structs::{PictrsImageMode, Settings}, settings::{
structs::{PictrsImageMode, Settings},
SETTINGS,
},
utils::{ utils::{
markdown::{markdown_check_for_blocked_urls, markdown_rewrite_image_links}, markdown::{image_links::markdown_rewrite_image_links, markdown_check_for_blocked_urls},
slurs::{build_slur_regex, remove_slurs}, slurs::{build_slur_regex, remove_slurs},
validation::clean_urls_in_text,
}, },
CACHE_DURATION_FEDERATION, CACHE_DURATION_FEDERATION,
}; };
@ -69,13 +79,7 @@ pub async fn is_mod_or_admin(
community_id: CommunityId, community_id: CommunityId,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
check_user_valid(person)?; check_user_valid(person)?;
CommunityView::check_is_mod_or_admin(pool, person.id, community_id).await
let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person.id, community_id).await?;
if !is_mod_or_admin {
Err(LemmyErrorType::NotAModOrAdmin)?
} else {
Ok(())
}
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@ -106,13 +110,7 @@ pub async fn check_community_mod_of_any_or_admin_action(
let person = &local_user_view.person; let person = &local_user_view.person;
check_user_valid(person)?; check_user_valid(person)?;
CommunityView::check_is_mod_of_any_or_admin(pool, person.id).await
let is_mod_of_any_or_admin = CommunityView::is_mod_of_any_or_admin(pool, person.id).await?;
if !is_mod_of_any_or_admin {
Err(LemmyErrorType::NotAModOrAdmin)?
} else {
Ok(())
}
} }
pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> { pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> {
@ -168,12 +166,9 @@ pub async fn update_read_comments(
person_id, person_id,
post_id, post_id,
read_comments, read_comments,
..PersonPostAggregatesForm::default()
}; };
PersonPostAggregates::upsert(pool, &person_post_agg_form) PersonPostAggregates::upsert(pool, &person_post_agg_form).await?;
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
Ok(()) Ok(())
} }
@ -191,63 +186,85 @@ pub fn check_user_valid(person: &Person) -> LemmyResult<()> {
} }
} }
/// Check if the user's email is verified if email verification is turned on
/// However, skip checking verification if the user is an admin
pub fn check_email_verified(
local_user_view: &LocalUserView,
site_view: &SiteView,
) -> LemmyResult<()> {
if !local_user_view.local_user.admin
&& site_view.local_site.require_email_verification
&& !local_user_view.local_user.email_verified
{
Err(LemmyErrorType::EmailNotVerified)?
}
Ok(())
}
pub async fn check_registration_application(
local_user_view: &LocalUserView,
local_site: &LocalSite,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
if (local_site.registration_mode == RegistrationMode::RequireApplication
|| local_site.registration_mode == RegistrationMode::Closed)
&& !local_user_view.local_user.accepted_application
&& !local_user_view.local_user.admin
{
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
// was processed (either accepted or denied).
let local_user_id = local_user_view.local_user.id;
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?;
if registration.admin_id.is_some() {
Err(LemmyErrorType::RegistrationDenied {
reason: registration.deny_reason,
})?
} else {
Err(LemmyErrorType::RegistrationApplicationIsPending)?
}
}
Ok(())
}
/// Checks that a normal user action (eg posting or voting) is allowed in a given community. /// Checks that a normal user action (eg posting or voting) is allowed in a given community.
/// ///
/// In particular it checks that neither the user nor community are banned or deleted, and that /// In particular it checks that neither the user nor community are banned or deleted, and that
/// the user isn't banned. /// the user isn't banned.
pub async fn check_community_user_action( pub async fn check_community_user_action(
person: &Person, person: &Person,
community_id: CommunityId, community: &Community,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
check_user_valid(person)?; check_user_valid(person)?;
check_community_deleted_removed(community_id, pool).await?; check_community_deleted_removed(community)?;
check_community_ban(person, community_id, pool).await?; CommunityPersonBanView::check(pool, person.id, community.id).await?;
CommunityFollowerView::check_private_community_action(pool, person.id, community).await?;
Ok(()) Ok(())
} }
async fn check_community_deleted_removed( pub fn check_community_deleted_removed(community: &Community) -> LemmyResult<()> {
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let community = Community::read(pool, community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
if community.deleted || community.removed { if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)? Err(LemmyErrorType::Deleted)?
} }
Ok(()) Ok(())
} }
async fn check_community_ban(
person: &Person,
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
// check if user was banned from site or community
let is_banned = CommunityPersonBanView::get(pool, person.id, community_id).await?;
if is_banned {
Err(LemmyErrorType::BannedFromCommunity)?
}
Ok(())
}
/// Check that the given user can perform a mod action in the community. /// Check that the given user can perform a mod action in the community.
/// ///
/// In particular it checks that he is an admin or mod, wasn't banned and the community isn't /// In particular it checks that he is an admin or mod, wasn't banned and the community isn't
/// removed/deleted. /// removed/deleted.
pub async fn check_community_mod_action( pub async fn check_community_mod_action(
person: &Person, person: &Person,
community_id: CommunityId, community: &Community,
allow_deleted: bool, allow_deleted: bool,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
is_mod_or_admin(pool, person, community_id).await?; is_mod_or_admin(pool, person, community.id).await?;
check_community_ban(person, community_id, pool).await?; CommunityPersonBanView::check(pool, person.id, community.id).await?;
// it must be possible to restore deleted community // it must be possible to restore deleted community
if !allow_deleted { if !allow_deleted {
check_community_deleted_removed(community_id, pool).await?; check_community_deleted_removed(community)?;
} }
Ok(()) Ok(())
} }
@ -269,51 +286,6 @@ pub fn check_comment_deleted_or_removed(comment: &Comment) -> LemmyResult<()> {
} }
} }
/// Throws an error if a recipient has blocked a person.
#[tracing::instrument(skip_all)]
pub async fn check_person_block(
my_id: PersonId,
potential_blocker_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = PersonBlock::read(pool, potential_blocker_id, my_id).await?;
if is_blocked {
Err(LemmyErrorType::PersonIsBlocked)?
} else {
Ok(())
}
}
/// Throws an error if a recipient has blocked a community.
#[tracing::instrument(skip_all)]
async fn check_community_block(
community_id: CommunityId,
person_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = CommunityBlock::read(pool, person_id, community_id).await?;
if is_blocked {
Err(LemmyErrorType::CommunityIsBlocked)?
} else {
Ok(())
}
}
/// Throws an error if a recipient has blocked an instance.
#[tracing::instrument(skip_all)]
async fn check_instance_block(
instance_id: InstanceId,
person_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = InstanceBlock::read(pool, person_id, instance_id).await?;
if is_blocked {
Err(LemmyErrorType::InstanceIsBlocked)?
} else {
Ok(())
}
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn check_person_instance_community_block( pub async fn check_person_instance_community_block(
my_id: PersonId, my_id: PersonId,
@ -322,19 +294,42 @@ pub async fn check_person_instance_community_block(
community_id: CommunityId, community_id: CommunityId,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
check_person_block(my_id, potential_blocker_id, pool).await?; PersonBlock::read(pool, potential_blocker_id, my_id).await?;
check_instance_block(community_instance_id, potential_blocker_id, pool).await?; InstanceBlock::read(pool, potential_blocker_id, community_instance_id).await?;
check_community_block(community_id, potential_blocker_id, pool).await?; CommunityBlock::read(pool, potential_blocker_id, community_id).await?;
Ok(()) Ok(())
} }
/// A vote item type used to check the vote mode.
pub enum VoteItem {
Post(PostId),
Comment(CommentId),
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> LemmyResult<()> { pub async fn check_local_vote_mode(
if score == -1 && !local_site.enable_downvotes { score: i16,
Err(LemmyErrorType::DownvotesAreDisabled)? vote_item: VoteItem,
} else { local_site: &LocalSite,
Ok(()) person_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let (downvote_setting, upvote_setting) = match vote_item {
VoteItem::Post(_) => (local_site.post_downvotes, local_site.post_upvotes),
VoteItem::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes),
};
let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable;
let upvote_fail = score == 1 && upvote_setting == FederationMode::Disable;
// Undo previous vote for item if new vote fails
if downvote_fail || upvote_fail {
match vote_item {
VoteItem::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?,
VoteItem::Comment(comment_id) => CommentLike::remove(pool, person_id, comment_id).await?,
};
} }
Ok(())
} }
/// Dont allow bots to do certain actions, like voting /// Dont allow bots to do certain actions, like voting
@ -359,6 +354,16 @@ pub fn check_private_instance(
} }
} }
/// If private messages are disabled, dont allow them to be sent / received
#[tracing::instrument(skip_all)]
pub fn check_private_messages_enabled(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
if !local_user_view.local_user.enable_private_messages {
Err(LemmyErrorType::CouldntCreatePrivateMessage)?
} else {
Ok(())
}
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn build_federated_instances( pub async fn build_federated_instances(
local_site: &LocalSite, local_site: &LocalSite,
@ -537,13 +542,6 @@ pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Re
.unwrap_or(None) .unwrap_or(None)
} }
pub fn local_site_opt_to_sensitive(local_site: &Option<LocalSite>) -> bool {
local_site
.as_ref()
.map(|site| site.enable_nsfw)
.unwrap_or(false)
}
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> { pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
static URL_BLOCKLIST: LazyLock<Cache<(), RegexSet>> = LazyLock::new(|| { static URL_BLOCKLIST: LazyLock<Cache<(), RegexSet>> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -667,7 +665,7 @@ pub async fn purge_image_posts_for_person(
/// Delete a local_user's images /// Delete a local_user's images
async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> {
if let Ok(Some(local_user)) = LocalUserView::read_person(&mut context.pool(), person_id).await { if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), person_id).await {
let pictrs_uploads = let pictrs_uploads =
LocalImageView::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id) LocalImageView::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id)
.await?; .await?;
@ -706,106 +704,189 @@ pub async fn purge_image_posts_for_community(
Ok(()) Ok(())
} }
pub async fn remove_user_data( /// Removes or restores user data.
pub async fn remove_or_restore_user_data(
mod_person_id: PersonId,
banned_person_id: PersonId, banned_person_id: PersonId,
removed: bool,
reason: &Option<String>,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let pool = &mut context.pool(); let pool = &mut context.pool();
// Purge user images
let person = Person::read(pool, banned_person_id) // Only these actions are possible when removing, not restoring
.await? if removed {
.ok_or(LemmyErrorType::CouldntFindPerson)?; // Purge user images
if let Some(avatar) = person.avatar { let person = Person::read(pool, banned_person_id).await?;
purge_image_from_pictrs(&avatar, context).await.ok(); if let Some(avatar) = person.avatar {
} purge_image_from_pictrs(&avatar, context).await.ok();
if let Some(banner) = person.banner { }
purge_image_from_pictrs(&banner, context).await.ok(); if let Some(banner) = person.banner {
purge_image_from_pictrs(&banner, context).await.ok();
}
// Update the fields to None
Person::update(
pool,
banned_person_id,
&PersonUpdateForm {
avatar: Some(None),
banner: Some(None),
bio: Some(None),
..Default::default()
},
)
.await?;
// Purge image posts
purge_image_posts_for_person(banned_person_id, context).await?;
// Communities
// Remove all communities where they're the top mod
// for now, remove the communities manually
let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?;
// Filter to only this banned users top communities
let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
.into_iter()
.filter(|fmc| fmc.moderator.id == banned_person_id)
.collect();
for first_mod_community in banned_user_first_communities {
let community_id = first_mod_community.community.id;
Community::update(
pool,
community_id,
&CommunityUpdateForm {
removed: Some(removed),
..Default::default()
},
)
.await?;
// Delete the community images
if let Some(icon) = first_mod_community.community.icon {
purge_image_from_pictrs(&icon, context).await.ok();
}
if let Some(banner) = first_mod_community.community.banner {
purge_image_from_pictrs(&banner, context).await.ok();
}
// Update the fields to None
Community::update(
pool,
community_id,
&CommunityUpdateForm {
icon: Some(None),
banner: Some(None),
..Default::default()
},
)
.await?;
}
} }
// Update the fields to None // Posts
Person::update( let removed_or_restored_posts =
Post::update_removed_for_creator(pool, banned_person_id, None, removed).await?;
create_modlog_entries_for_removed_or_restored_posts(
pool, pool,
banned_person_id, mod_person_id,
&PersonUpdateForm { removed_or_restored_posts.iter().map(|r| r.id).collect(),
avatar: Some(None), removed,
banner: Some(None), reason,
bio: Some(None),
..Default::default()
},
) )
.await?; .await?;
// Posts
Post::update_removed_for_creator(pool, banned_person_id, None, true).await?;
// Purge image posts
purge_image_posts_for_person(banned_person_id, context).await?;
// Communities
// Remove all communities where they're the top mod
// for now, remove the communities manually
let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?;
// Filter to only this banned users top communities
let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
.into_iter()
.filter(|fmc| fmc.moderator.id == banned_person_id)
.collect();
for first_mod_community in banned_user_first_communities {
let community_id = first_mod_community.community.id;
Community::update(
pool,
community_id,
&CommunityUpdateForm {
removed: Some(true),
..Default::default()
},
)
.await?;
// Delete the community images
if let Some(icon) = first_mod_community.community.icon {
purge_image_from_pictrs(&icon, context).await.ok();
}
if let Some(banner) = first_mod_community.community.banner {
purge_image_from_pictrs(&banner, context).await.ok();
}
// Update the fields to None
Community::update(
pool,
community_id,
&CommunityUpdateForm {
icon: Some(None),
banner: Some(None),
..Default::default()
},
)
.await?;
}
// Comments // Comments
Comment::update_removed_for_creator(pool, banned_person_id, true).await?; let removed_or_restored_comments =
Comment::update_removed_for_creator(pool, banned_person_id, removed).await?;
create_modlog_entries_for_removed_or_restored_comments(
pool,
mod_person_id,
removed_or_restored_comments.iter().map(|r| r.id).collect(),
removed,
reason,
)
.await?;
Ok(()) Ok(())
} }
pub async fn remove_user_data_in_community( async fn create_modlog_entries_for_removed_or_restored_posts(
pool: &mut DbPool<'_>,
mod_person_id: PersonId,
post_ids: Vec<PostId>,
removed: bool,
reason: &Option<String>,
) -> LemmyResult<()> {
// Build the forms
let forms = post_ids
.iter()
.map(|&post_id| ModRemovePostForm {
mod_person_id,
post_id,
removed: Some(removed),
reason: reason.clone(),
})
.collect();
ModRemovePost::create_multiple(pool, &forms).await?;
Ok(())
}
async fn create_modlog_entries_for_removed_or_restored_comments(
pool: &mut DbPool<'_>,
mod_person_id: PersonId,
comment_ids: Vec<CommentId>,
removed: bool,
reason: &Option<String>,
) -> LemmyResult<()> {
// Build the forms
let forms = comment_ids
.iter()
.map(|&comment_id| ModRemoveCommentForm {
mod_person_id,
comment_id,
removed: Some(removed),
reason: reason.clone(),
})
.collect();
ModRemoveComment::create_multiple(pool, &forms).await?;
Ok(())
}
pub async fn remove_or_restore_user_data_in_community(
community_id: CommunityId, community_id: CommunityId,
mod_person_id: PersonId,
banned_person_id: PersonId, banned_person_id: PersonId,
remove: bool,
reason: &Option<String>,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
// Posts // Posts
Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?; let posts =
Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), remove).await?;
create_modlog_entries_for_removed_or_restored_posts(
pool,
mod_person_id,
posts.iter().map(|r| r.id).collect(),
remove,
reason,
)
.await?;
// Comments // Comments
// TODO Diesel doesn't allow updates with joins, so this has to be a loop // TODO Diesel doesn't allow updates with joins, so this has to be a loop
let site = Site::read_local(pool).await?;
let comments = CommentQuery { let comments = CommentQuery {
creator_id: Some(banned_person_id), creator_id: Some(banned_person_id),
community_id: Some(community_id), community_id: Some(community_id),
..Default::default() ..Default::default()
} }
.list(pool) .list(&site, pool)
.await?; .await?;
for comment_view in &comments { for comment_view in &comments {
@ -814,22 +895,29 @@ pub async fn remove_user_data_in_community(
pool, pool,
comment_id, comment_id,
&CommentUpdateForm { &CommentUpdateForm {
removed: Some(true), removed: Some(remove),
..Default::default() ..Default::default()
}, },
) )
.await?; .await?;
} }
create_modlog_entries_for_removed_or_restored_comments(
pool,
mod_person_id,
comments.iter().map(|r| r.comment.id).collect(),
remove,
reason,
)
.await?;
Ok(()) Ok(())
} }
pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> {
let pool = &mut context.pool(); let pool = &mut context.pool();
let person = Person::read(pool, person_id) let person = Person::read(pool, person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
// Delete their local images, if they're a local user // Delete their local images, if they're a local user
delete_local_user_images(person_id, context).await.ok(); delete_local_user_images(person_id, context).await.ok();
@ -858,6 +946,11 @@ pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) ->
// Leave communities they mod // Leave communities they mod
CommunityModerator::leave_all_communities(pool, person_id).await?; CommunityModerator::leave_all_communities(pool, person_id).await?;
// Delete the oauth accounts linked to the local user
if let Ok(local_user) = LocalUserView::read_person(pool, person_id).await {
OAuthAccount::delete_user_accounts(pool, local_user.local_user.id).await?;
}
Person::delete_account(pool, person_id).await?; Person::delete_account(pool, person_id).await?;
Ok(()) Ok(())
@ -892,12 +985,8 @@ pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{actor_id}/followers"))?.into()) Ok(Url::parse(&format!("{actor_id}/followers"))?.into())
} }
pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> { pub fn generate_inbox_url() -> LemmyResult<DbUrl> {
Ok(Url::parse(&format!("{actor_id}/inbox"))?.into()) let url = format!("{}/inbox", SETTINGS.get_protocol_and_hostname());
}
pub fn generate_shared_inbox_url(settings: &Settings) -> LemmyResult<DbUrl> {
let url = format!("{}/inbox", settings.get_protocol_and_hostname());
Ok(Url::parse(&url)?.into()) Ok(Url::parse(&url)?.into())
} }
@ -940,6 +1029,18 @@ fn limit_expire_time(expires: DateTime<Utc>) -> LemmyResult<Option<DateTime<Utc>
} }
} }
#[tracing::instrument(skip_all)]
pub fn check_conflicting_like_filters(
liked_only: Option<bool>,
disliked_only: Option<bool>,
) -> LemmyResult<()> {
if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() {
Err(LemmyErrorType::ContradictingFilters)?
} else {
Ok(())
}
}
pub async fn process_markdown( pub async fn process_markdown(
text: &str, text: &str,
slur_regex: &Option<Regex>, slur_regex: &Option<Regex>,
@ -947,11 +1048,13 @@ pub async fn process_markdown(
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<String> { ) -> LemmyResult<String> {
let text = remove_slurs(text, slur_regex); let text = remove_slurs(text, slur_regex);
let text = clean_urls_in_text(&text);
markdown_check_for_blocked_urls(&text, url_blocklist)?; markdown_check_for_blocked_urls(&text, url_blocklist)?;
if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages { if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages {
let (text, links) = markdown_rewrite_image_links(text); let (text, links) = markdown_rewrite_image_links(text);
RemoteImage::create(&mut context.pool(), links.clone()).await?;
// Create images and image detail rows // Create images and image detail rows
for link in links { for link in links {
@ -961,7 +1064,7 @@ pub async fn process_markdown(
let proxied = let proxied =
build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?; build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
let details_form = details.build_image_details_form(&proxied); let details_form = details.build_image_details_form(&proxied);
RemoteImage::create(&mut context.pool(), &details_form).await?; ImageDetails::create(&mut context.pool(), &details_form).await?;
} }
} }
Ok(text) Ok(text)
@ -997,13 +1100,15 @@ async fn proxy_image_link_internal(
if link.domain() == Some(&context.settings().hostname) { if link.domain() == Some(&context.settings().hostname) {
Ok(link.into()) Ok(link.into())
} else if image_mode == PictrsImageMode::ProxyAllImages { } else if image_mode == PictrsImageMode::ProxyAllImages {
RemoteImage::create(&mut context.pool(), vec![link.clone()]).await?;
let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?; let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
// This should fail softly, since pictrs might not even be running // This should fail softly, since pictrs might not even be running
let details_res = fetch_pictrs_proxied_image_details(&link, context).await; let details_res = fetch_pictrs_proxied_image_details(&link, context).await;
if let Ok(details) = details_res { if let Ok(details) = details_res {
let details_form = details.build_image_details_form(&proxied); let details_form = details.build_image_details_form(&proxied);
RemoteImage::create(&mut context.pool(), &details_form).await?; ImageDetails::create(&mut context.pool(), &details_form).await?;
}; };
Ok(proxied.into()) Ok(proxied.into())
@ -1014,7 +1119,7 @@ async fn proxy_image_link_internal(
/// Rewrite a link to go through `/api/v3/image_proxy` endpoint. This is only for remote urls and /// Rewrite a link to go through `/api/v3/image_proxy` endpoint. This is only for remote urls and
/// if image_proxy setting is enabled. /// if image_proxy setting is enabled.
pub(crate) async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult<DbUrl> { pub async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult<DbUrl> {
proxy_image_link_internal( proxy_image_link_internal(
link, link,
context.settings().pictrs_config()?.image_mode(), context.settings().pictrs_config()?.image_mode(),
@ -1071,11 +1176,20 @@ fn build_proxied_image_url(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;
use lemmy_db_schema::source::{
comment::CommentInsertForm,
community::CommunityInsertForm,
person::PersonInsertForm,
post::PostInsertForm,
};
use lemmy_db_views_moderator::structs::{
ModRemoveCommentView,
ModRemovePostView,
ModlogListParams,
};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serial_test::serial; use serial_test::serial;
@ -1097,48 +1211,42 @@ mod tests {
} }
#[test] #[test]
fn test_limit_ban_term() { fn test_limit_ban_term() -> LemmyResult<()> {
// Ban expires in past, should throw error // Ban expires in past, should throw error
assert!(limit_expire_time(Utc::now() - Days::new(5)).is_err()); assert!(limit_expire_time(Utc::now() - Days::new(5)).is_err());
// Legitimate ban term, return same value // Legitimate ban term, return same value
let fourteen_days = Utc::now() + Days::new(14); let fourteen_days = Utc::now() + Days::new(14);
assert_eq!( assert_eq!(limit_expire_time(fourteen_days)?, Some(fourteen_days));
limit_expire_time(fourteen_days).unwrap(),
Some(fourteen_days)
);
let nine_years = Utc::now() + Days::new(365 * 9); let nine_years = Utc::now() + Days::new(365 * 9);
assert_eq!(limit_expire_time(nine_years).unwrap(), Some(nine_years)); assert_eq!(limit_expire_time(nine_years)?, Some(nine_years));
// Too long ban term, changes to None (permanent ban) // Too long ban term, changes to None (permanent ban)
assert_eq!( assert_eq!(limit_expire_time(Utc::now() + Days::new(365 * 11))?, None);
limit_expire_time(Utc::now() + Days::new(365 * 11)).unwrap(),
None Ok(())
);
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_proxy_image_link() { async fn test_proxy_image_link() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
// image from local domain is unchanged // image from local domain is unchanged
let local_url = Url::parse("http://lemmy-alpha/image.png").unwrap(); let local_url = Url::parse("http://lemmy-alpha/image.png")?;
let proxied = let proxied =
proxy_image_link_internal(local_url.clone(), PictrsImageMode::ProxyAllImages, &context) proxy_image_link_internal(local_url.clone(), PictrsImageMode::ProxyAllImages, &context)
.await .await?;
.unwrap();
assert_eq!(&local_url, proxied.inner()); assert_eq!(&local_url, proxied.inner());
// image from remote domain is proxied // image from remote domain is proxied
let remote_image = Url::parse("http://lemmy-beta/image.png").unwrap(); let remote_image = Url::parse("http://lemmy-beta/image.png")?;
let proxied = proxy_image_link_internal( let proxied = proxy_image_link_internal(
remote_image.clone(), remote_image.clone(),
PictrsImageMode::ProxyAllImages, PictrsImageMode::ProxyAllImages,
&context, &context,
) )
.await .await?;
.unwrap();
assert_eq!( assert_eq!(
"https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png", "https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png",
proxied.as_str() proxied.as_str()
@ -1149,7 +1257,161 @@ mod tests {
assert!( assert!(
RemoteImage::validate(&mut context.pool(), remote_image.into()) RemoteImage::validate(&mut context.pool(), remote_image.into())
.await .await
.is_err() .is_ok()
); );
Ok(())
}
#[tokio::test]
#[serial]
async fn test_mod_remove_or_restore_data() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_mod = PersonInsertForm::test_form(inserted_instance.id, "modder");
let inserted_mod = Person::create(pool, &new_mod).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "chrimbus");
let inserted_person = Person::create(pool, &new_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
"mod_community crepes".to_string(),
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await?;
let post_form_1 = PostInsertForm::new(
"A test post tubular".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post_1 = Post::create(pool, &post_form_1).await?;
let post_form_2 = PostInsertForm::new(
"A test post radical".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post_2 = Post::create(pool, &post_form_2).await?;
let comment_form_1 = CommentInsertForm::new(
inserted_person.id,
inserted_post_1.id,
"A test comment tubular".into(),
);
let _inserted_comment_1 = Comment::create(pool, &comment_form_1, None).await?;
let comment_form_2 = CommentInsertForm::new(
inserted_person.id,
inserted_post_2.id,
"A test comment radical".into(),
);
let _inserted_comment_2 = Comment::create(pool, &comment_form_2, None).await?;
// Remove the user data
remove_or_restore_user_data(
inserted_mod.id,
inserted_person.id,
true,
&Some("a remove reason".to_string()),
&context,
)
.await?;
// Verify that their posts and comments are removed.
let params = ModlogListParams {
community_id: None,
mod_person_id: None,
other_person_id: None,
post_id: None,
comment_id: None,
page: None,
limit: None,
hide_modlog_names: false,
};
// Posts
let post_modlog = ModRemovePostView::list(pool, params).await?;
assert_eq!(2, post_modlog.len());
let mod_removed_posts = post_modlog
.iter()
.map(|p| p.mod_remove_post.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![true, true], mod_removed_posts);
let removed_posts = post_modlog
.iter()
.map(|p| p.post.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![true, true], removed_posts);
// Comments
let comment_modlog = ModRemoveCommentView::list(pool, params).await?;
assert_eq!(2, comment_modlog.len());
let mod_removed_comments = comment_modlog
.iter()
.map(|p| p.mod_remove_comment.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![true, true], mod_removed_comments);
let removed_comments = comment_modlog
.iter()
.map(|p| p.comment.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![true, true], removed_comments);
// Now restore the content, and make sure it got appended
remove_or_restore_user_data(
inserted_mod.id,
inserted_person.id,
false,
&Some("a restore reason".to_string()),
&context,
)
.await?;
// Posts
let post_modlog = ModRemovePostView::list(pool, params).await?;
assert_eq!(4, post_modlog.len());
let mod_restored_posts = post_modlog
.iter()
.map(|p| p.mod_remove_post.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![false, false, true, true], mod_restored_posts);
let restored_posts = post_modlog
.iter()
.map(|p| p.post.removed)
.collect::<Vec<bool>>();
// All of these will be false, cause its the current state of the post
assert_eq!(vec![false, false, false, false], restored_posts);
// Comments
let comment_modlog = ModRemoveCommentView::list(pool, params).await?;
assert_eq!(4, comment_modlog.len());
let mod_restored_comments = comment_modlog
.iter()
.map(|p| p.mod_remove_comment.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![false, false, true, true], mod_restored_comments);
let restored_comments = comment_modlog
.iter()
.map(|p| p.comment.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![false, false, false, false], restored_comments);
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
} }
} }

View file

@ -27,8 +27,12 @@ futures.workspace = true
uuid = { workspace = true } uuid = { workspace = true }
moka.workspace = true moka.workspace = true
anyhow.workspace = true anyhow.workspace = true
webmention = "0.5.0" chrono.workspace = true
webmention = "0.6.0"
accept-language = "3.1.0" accept-language = "3.1.0"
serde_json = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
[package.metadata.cargo-machete] [package.metadata.cargo-shear]
ignored = ["futures"] ignored = ["futures"]

View file

@ -30,10 +30,9 @@ use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field},
MAX_COMMENT_DEPTH_LIMIT,
}; };
const MAX_COMMENT_DEPTH_LIMIT: usize = 100;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn create_comment( pub async fn create_comment(
data: Json<CreateComment>, data: Json<CreateComment>,
@ -57,13 +56,17 @@ pub async fn create_comment(
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
true, true,
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindPost)?;
let post = post_view.post; let post = post_view.post;
let community_id = post_view.community.id; let community_id = post_view.community.id;
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?; check_community_user_action(
&local_user_view.person,
&post_view.community,
&mut context.pool(),
)
.await?;
check_post_deleted_or_removed(&post)?; check_post_deleted_or_removed(&post)?;
// Check if post is locked, no new comments // Check if post is locked, no new comments
@ -79,8 +82,7 @@ pub async fn create_comment(
Comment::read(&mut context.pool(), parent_id).await.ok() Comment::read(&mut context.pool(), parent_id).await.ok()
} else { } else {
None None
} };
.flatten();
// If there's a parent_id, check to make sure that comment is in that post // If there's a parent_id, check to make sure that comment is in that post
// Strange issue where sometimes the post ID of the parent comment is incorrect // Strange issue where sometimes the post ID of the parent comment is incorrect
@ -91,16 +93,9 @@ pub async fn create_comment(
check_comment_depth(parent)?; check_comment_depth(parent)?;
} }
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
data.language_id,
community_id,
)
.await?;
// attempt to set default language if none was provided // attempt to set default language if none was provided
let language_id = match data.language_id { let language_id = match data.language_id {
Some(lid) => Some(lid), Some(lid) => lid,
None => { None => {
default_post_language( default_post_language(
&mut context.pool(), &mut context.pool(),
@ -111,12 +106,13 @@ pub async fn create_comment(
} }
}; };
let comment_form = CommentInsertForm::builder() CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id)
.content(content.clone()) .await?;
.post_id(data.post_id)
.creator_id(local_user_view.person.id) let comment_form = CommentInsertForm {
.language_id(language_id) language_id: Some(language_id),
.build(); ..CommentInsertForm::new(local_user_view.person.id, data.post_id, content.clone())
};
// Create the comment // Create the comment
let parent_path = parent_opt.clone().map(|t| t.path); let parent_path = parent_opt.clone().map(|t| t.path);
@ -141,7 +137,6 @@ pub async fn create_comment(
// You like your own comment by default // You like your own comment by default
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
comment_id: inserted_comment.id, comment_id: inserted_comment.id,
post_id: post.id,
person_id: local_user_view.person.id, person_id: local_user_view.person.id,
score: 1, score: 1,
}; };
@ -153,8 +148,7 @@ pub async fn create_comment(
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::CreateComment(inserted_comment.clone()), SendActivityData::CreateComment(inserted_comment.clone()),
&context, &context,
) )?;
.await?;
// Update the read comments, so your own new comment doesn't appear as a +1 unread // Update the read comments, so your own new comment doesn't appear as a +1 unread
update_read_comments( update_read_comments(

View file

@ -26,8 +26,7 @@ pub async fn delete_comment(
comment_id, comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
// Dont delete it if its already been deleted. // Dont delete it if its already been deleted.
if orig_comment.comment.deleted == data.deleted { if orig_comment.comment.deleted == data.deleted {
@ -36,7 +35,7 @@ pub async fn delete_comment(
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
orig_comment.community.id, &orig_comment.community,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -77,8 +76,7 @@ pub async fn delete_comment(
orig_comment.community, orig_comment.community,
), ),
&context, &context,
) )?;
.await?;
Ok(Json( Ok(Json(
build_comment_response( build_comment_response(

View file

@ -31,12 +31,11 @@ pub async fn remove_comment(
comment_id, comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
orig_comment.community.id, &orig_comment.community,
false, false,
&mut context.pool(), &mut context.pool(),
) )
@ -100,8 +99,7 @@ pub async fn remove_comment(
reason: data.reason.clone(), reason: data.reason.clone(),
}, },
&context, &context,
) )?;
.await?;
Ok(Json( Ok(Json(
build_comment_response( build_comment_response(

View file

@ -41,12 +41,11 @@ pub async fn update_comment(
comment_id, comment_id,
Some(&local_user_view.local_user), Some(&local_user_view.local_user),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
orig_comment.community.id, &orig_comment.community,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -56,13 +55,14 @@ pub async fn update_comment(
Err(LemmyErrorType::NoCommentEditAllowed)? Err(LemmyErrorType::NoCommentEditAllowed)?
} }
let language_id = data.language_id; if let Some(language_id) = data.language_id {
CommunityLanguage::is_allowed_community_language( CommunityLanguage::is_allowed_community_language(
&mut context.pool(), &mut context.pool(),
language_id, language_id,
orig_comment.community.id, orig_comment.community.id,
) )
.await?; .await?;
}
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
@ -98,8 +98,7 @@ pub async fn update_comment(
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::UpdateComment(updated_comment.clone()), SendActivityData::UpdateComment(updated_comment.clone()),
&context, &context,
) )?;
.await?;
Ok(Json( Ok(Json(
build_comment_response( build_comment_response(

View file

@ -1,3 +1,4 @@
use super::check_community_visibility_allowed;
use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair}; use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
@ -8,7 +9,6 @@ use lemmy_api_common::{
generate_followers_url, generate_followers_url,
generate_inbox_url, generate_inbox_url,
generate_local_apub_endpoint, generate_local_apub_endpoint,
generate_shared_inbox_url,
get_url_blocklist, get_url_blocklist,
is_admin, is_admin,
local_site_to_slur_regex, local_site_to_slur_regex,
@ -24,6 +24,7 @@ use lemmy_db_schema::{
Community, Community,
CommunityFollower, CommunityFollower,
CommunityFollowerForm, CommunityFollowerForm,
CommunityFollowerState,
CommunityInsertForm, CommunityInsertForm,
CommunityModerator, CommunityModerator,
CommunityModeratorForm, CommunityModeratorForm,
@ -37,7 +38,11 @@ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{ utils::{
slurs::check_slurs, slurs::check_slurs,
validation::{is_valid_actor_name, is_valid_body_field}, validation::{
is_valid_actor_name,
is_valid_body_field,
site_or_community_description_length_check,
},
}, },
}; };
@ -47,9 +52,7 @@ pub async fn create_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> LemmyResult<Json<CommunityResponse>> {
let site_view = SiteView::read_local(&mut context.pool()) let site_view = SiteView::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = site_view.local_site; let local_site = site_view.local_site;
if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() { if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
@ -60,8 +63,18 @@ pub async fn create_community(
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
check_slurs(&data.name, &slur_regex)?; check_slurs(&data.name, &slur_regex)?;
check_slurs(&data.title, &slur_regex)?; check_slurs(&data.title, &slur_regex)?;
let description = let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?;
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
// Ensure that the sidebar has fewer than the max num characters...
if let Some(sidebar) = &sidebar {
is_valid_body_field(sidebar, false)?;
}
let description = data.description.clone();
if let Some(desc) = &description {
site_or_community_description_length_check(desc)?;
check_slurs(desc, &slur_regex)?;
}
let icon = diesel_url_create(data.icon.as_deref())?; let icon = diesel_url_create(data.icon.as_deref())?;
let icon = proxy_image_link_api(icon, &context).await?; let icon = proxy_image_link_api(icon, &context).await?;
@ -75,6 +88,8 @@ pub async fn create_community(
is_valid_body_field(desc, false)?; is_valid_body_field(desc, false)?;
} }
check_community_visibility_allowed(data.visibility, &local_user_view)?;
// Double check for duplicate community actor_ids // Double check for duplicate community actor_ids
let community_actor_id = generate_local_apub_endpoint( let community_actor_id = generate_local_apub_endpoint(
EndpointType::Community, EndpointType::Community,
@ -90,23 +105,25 @@ pub async fn create_community(
// When you create a community, make sure the user becomes a moderator and a follower // When you create a community, make sure the user becomes a moderator and a follower
let keypair = generate_actor_keypair()?; let keypair = generate_actor_keypair()?;
let community_form = CommunityInsertForm::builder() let community_form = CommunityInsertForm {
.name(data.name.clone()) sidebar,
.title(data.title.clone()) description,
.description(description) icon,
.icon(icon) banner,
.banner(banner) nsfw: data.nsfw,
.nsfw(data.nsfw) actor_id: Some(community_actor_id.clone()),
.actor_id(Some(community_actor_id.clone())) private_key: Some(keypair.private_key),
.private_key(Some(keypair.private_key)) followers_url: Some(generate_followers_url(&community_actor_id)?),
.public_key(keypair.public_key) inbox_url: Some(generate_inbox_url()?),
.followers_url(Some(generate_followers_url(&community_actor_id)?)) posting_restricted_to_mods: data.posting_restricted_to_mods,
.inbox_url(Some(generate_inbox_url(&community_actor_id)?)) visibility: data.visibility,
.shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?)) ..CommunityInsertForm::new(
.posting_restricted_to_mods(data.posting_restricted_to_mods) site_view.site.instance_id,
.instance_id(site_view.site.instance_id) data.name.clone(),
.visibility(data.visibility) data.title.clone(),
.build(); keypair.public_key,
)
};
let inserted_community = Community::create(&mut context.pool(), &community_form) let inserted_community = Community::create(&mut context.pool(), &community_form)
.await .await
@ -126,7 +143,8 @@ pub async fn create_community(
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id, community_id: inserted_community.id,
person_id: local_user_view.person.id, person_id: local_user_view.person.id,
pending: false, state: Some(CommunityFollowerState::Accepted),
approver_id: None,
}; };
CommunityFollower::follow(&mut context.pool(), &community_follower_form) CommunityFollower::follow(&mut context.pool(), &community_follower_form)

View file

@ -22,13 +22,13 @@ pub async fn delete_community(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> LemmyResult<Json<CommunityResponse>> {
// Fetch the community mods // Fetch the community mods
let community_id = data.community_id;
let community_mods = let community_mods =
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; CommunityModeratorView::for_community(&mut context.pool(), data.community_id).await?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
community_id, &community,
true, true,
&mut context.pool(), &mut context.pool(),
) )
@ -54,8 +54,7 @@ pub async fn delete_community(
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::DeleteCommunity(local_user_view.person.clone(), community, data.deleted), SendActivityData::DeleteCommunity(local_user_view.person.clone(), community, data.deleted),
&context, &context,
) )?;
.await?;
build_community_response(&context, local_user_view, community_id).await build_community_response(&context, local_user_view, community_id).await
} }

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_views_actor::community_view::CommunityQuery; use lemmy_db_views_actor::community_view::CommunityQuery;
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn list_communities( pub async fn list_communities(
@ -14,9 +14,7 @@ pub async fn list_communities(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<ListCommunitiesResponse>> { ) -> LemmyResult<Json<ListCommunitiesResponse>> {
let local_site = SiteView::read_local(&mut context.pool()) let local_site = SiteView::read_local(&mut context.pool()).await?;
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let is_admin = local_user_view let is_admin = local_user_view
.as_ref() .as_ref()
.map(|luv| is_admin(luv).is_ok()) .map(|luv| is_admin(luv).is_ok())

View file

@ -1,5 +1,22 @@
use lemmy_api_common::utils::is_admin;
use lemmy_db_schema::CommunityVisibility;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
pub mod create; pub mod create;
pub mod delete; pub mod delete;
pub mod list; pub mod list;
pub mod remove; pub mod remove;
pub mod update; pub mod update;
/// For now only admins can make communities private, in order to prevent abuse.
/// Need to implement admin approval for new communities to get rid of this.
fn check_community_visibility_allowed(
visibility: Option<CommunityVisibility>,
local_user_view: &LocalUserView,
) -> LemmyResult<()> {
if visibility == Some(lemmy_db_schema::CommunityVisibility::Private) {
is_admin(local_user_view)?;
}
Ok(())
}

View file

@ -23,9 +23,10 @@ pub async fn remove_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> LemmyResult<Json<CommunityResponse>> {
let community = Community::read(&mut context.pool(), data.community_id).await?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
data.community_id, &community,
true, true,
&mut context.pool(), &mut context.pool(),
) )
@ -65,8 +66,7 @@ pub async fn remove_community(
removed: data.removed, removed: data.removed,
}, },
&context, &context,
) )?;
.await?;
build_community_response(&context, local_user_view, community_id).await build_community_response(&context, local_user_view, community_id).await
} }

View file

@ -1,3 +1,4 @@
use super::check_community_visibility_allowed;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
@ -41,19 +42,20 @@ pub async fn update_community(
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
check_slurs_opt(&data.title, &slur_regex)?; check_slurs_opt(&data.title, &slur_regex)?;
let description = diesel_string_update( let sidebar = diesel_string_update(
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context) process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context)
.await? .await?
.as_deref(), .as_deref(),
); );
if let Some(Some(desc)) = &description { if let Some(Some(sidebar)) = &sidebar {
is_valid_body_field(desc, false)?; is_valid_body_field(sidebar, false)?;
} }
let old_community = Community::read(&mut context.pool(), data.community_id) check_community_visibility_allowed(data.visibility, &local_user_view)?;
.await? let description = diesel_string_update(data.description.as_deref());
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let old_community = Community::read(&mut context.pool(), data.community_id).await?;
let icon = diesel_url_update(data.icon.as_deref())?; let icon = diesel_url_update(data.icon.as_deref())?;
replace_image(&icon, &old_community.icon, &context).await?; replace_image(&icon, &old_community.icon, &context).await?;
@ -66,7 +68,7 @@ pub async fn update_community(
// Verify its a mod (only mods can edit it) // Verify its a mod (only mods can edit it)
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
data.community_id, &old_community,
false, false,
&mut context.pool(), &mut context.pool(),
) )
@ -86,6 +88,7 @@ pub async fn update_community(
let community_form = CommunityUpdateForm { let community_form = CommunityUpdateForm {
title: data.title.clone(), title: data.title.clone(),
sidebar,
description, description,
icon, icon,
banner, banner,
@ -104,8 +107,7 @@ pub async fn update_community(
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::UpdateCommunity(local_user_view.person.clone(), community), SendActivityData::UpdateCommunity(local_user_view.person.clone(), community),
&context, &context,
) )?;
.await?;
build_community_response(&context, local_user_view, community_id).await build_community_response(&context, local_user_view, community_id).await
} }

View file

@ -5,10 +5,12 @@ use lemmy_api_common::{
custom_emoji::{CreateCustomEmoji, CustomEmojiResponse}, custom_emoji::{CreateCustomEmoji, CustomEmojiResponse},
utils::is_admin, utils::is_admin,
}; };
use lemmy_db_schema::source::{ use lemmy_db_schema::{
custom_emoji::{CustomEmoji, CustomEmojiInsertForm}, source::{
custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, custom_emoji::{CustomEmoji, CustomEmojiInsertForm},
local_site::LocalSite, custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm},
},
traits::Crud,
}; };
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; use lemmy_db_views::structs::{CustomEmojiView, LocalUserView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
@ -19,24 +21,20 @@ pub async fn create_custom_emoji(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CustomEmojiResponse>> { ) -> LemmyResult<Json<CustomEmojiResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
// Make sure user is an admin // Make sure user is an admin
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let emoji_form = CustomEmojiInsertForm::builder() let emoji_form = CustomEmojiInsertForm::new(
.local_site_id(local_site.id) data.shortcode.to_lowercase().trim().to_string(),
.shortcode(data.shortcode.to_lowercase().trim().to_string()) data.clone().image_url.into(),
.alt_text(data.alt_text.to_string()) data.alt_text.to_string(),
.category(data.category.to_string()) data.category.to_string(),
.image_url(data.clone().image_url.into()) );
.build();
let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?; let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?;
let mut keywords = vec![]; let mut keywords = vec![];
for keyword in &data.keywords { for keyword in &data.keywords {
let keyword_form = CustomEmojiKeywordInsertForm::builder() let keyword_form =
.custom_emoji_id(emoji.id) CustomEmojiKeywordInsertForm::new(emoji.id, keyword.to_lowercase().trim().to_string());
.keyword(keyword.to_lowercase().trim().to_string())
.build();
keywords.push(keyword_form); keywords.push(keyword_form);
} }
CustomEmojiKeyword::create(&mut context.pool(), keywords).await?; CustomEmojiKeyword::create(&mut context.pool(), keywords).await?;

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
utils::is_admin, utils::is_admin,
SuccessResponse, SuccessResponse,
}; };
use lemmy_db_schema::source::custom_emoji::CustomEmoji; use lemmy_db_schema::{source::custom_emoji::CustomEmoji, traits::Crud};
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;

View file

@ -0,0 +1,25 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{ListCustomEmojis, ListCustomEmojisResponse},
};
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView};
use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))]
pub async fn list_custom_emojis(
data: Query<ListCustomEmojis>,
local_user_view: Option<LocalUserView>,
context: Data<LemmyContext>,
) -> Result<Json<ListCustomEmojisResponse>, LemmyError> {
let custom_emojis = CustomEmojiView::list(
&mut context.pool(),
&data.category,
data.page,
data.limit,
data.ignore_page_limits.unwrap_or(false),
)
.await?;
Ok(Json(ListCustomEmojisResponse { custom_emojis }))
}

View file

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

View file

@ -5,10 +5,12 @@ use lemmy_api_common::{
custom_emoji::{CustomEmojiResponse, EditCustomEmoji}, custom_emoji::{CustomEmojiResponse, EditCustomEmoji},
utils::is_admin, utils::is_admin,
}; };
use lemmy_db_schema::source::{ use lemmy_db_schema::{
custom_emoji::{CustomEmoji, CustomEmojiUpdateForm}, source::{
custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, custom_emoji::{CustomEmoji, CustomEmojiUpdateForm},
local_site::LocalSite, custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm},
},
traits::Crud,
}; };
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; use lemmy_db_views::structs::{CustomEmojiView, LocalUserView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
@ -19,24 +21,20 @@ pub async fn update_custom_emoji(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CustomEmojiResponse>> { ) -> LemmyResult<Json<CustomEmojiResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
// Make sure user is an admin // Make sure user is an admin
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let emoji_form = CustomEmojiUpdateForm::builder() let emoji_form = CustomEmojiUpdateForm::new(
.local_site_id(local_site.id) data.clone().image_url.into(),
.alt_text(data.alt_text.to_string()) data.alt_text.to_string(),
.category(data.category.to_string()) data.category.to_string(),
.image_url(data.clone().image_url.into()) );
.build();
let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?; let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?;
CustomEmojiKeyword::delete(&mut context.pool(), data.id).await?; CustomEmojiKeyword::delete(&mut context.pool(), data.id).await?;
let mut keywords = vec![]; let mut keywords = vec![];
for keyword in &data.keywords { for keyword in &data.keywords {
let keyword_form = CustomEmojiKeywordInsertForm::builder() let keyword_form =
.custom_emoji_id(emoji.id) CustomEmojiKeywordInsertForm::new(emoji.id, keyword.to_lowercase().trim().to_string());
.keyword(keyword.to_lowercase().trim().to_string())
.build();
keywords.push(keyword_form); keywords.push(keyword_form);
} }
CustomEmojiKeyword::create(&mut context.pool(), keywords).await?; CustomEmojiKeyword::create(&mut context.pool(), keywords).await?;

View file

@ -1,7 +1,9 @@
pub mod comment; pub mod comment;
pub mod community; pub mod community;
pub mod custom_emoji; pub mod custom_emoji;
pub mod oauth_provider;
pub mod post; pub mod post;
pub mod private_message; pub mod private_message;
pub mod site; pub mod site;
pub mod tagline;
pub mod user; pub mod user;

View file

@ -0,0 +1,42 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
oauth_provider::CreateOAuthProvider,
utils::is_admin,
};
use lemmy_db_schema::{
source::oauth_provider::{OAuthProvider, OAuthProviderInsertForm},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
use url::Url;
#[tracing::instrument(skip(context))]
pub async fn create_oauth_provider(
data: Json<CreateOAuthProvider>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<OAuthProvider>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;
let cloned_data = data.clone();
let oauth_provider_form = OAuthProviderInsertForm {
display_name: cloned_data.display_name,
issuer: Url::parse(&cloned_data.issuer)?.into(),
authorization_endpoint: Url::parse(&cloned_data.authorization_endpoint)?.into(),
token_endpoint: Url::parse(&cloned_data.token_endpoint)?.into(),
userinfo_endpoint: Url::parse(&cloned_data.userinfo_endpoint)?.into(),
id_claim: cloned_data.id_claim,
client_id: data.client_id.to_string(),
client_secret: data.client_secret.to_string(),
scopes: data.scopes.to_string(),
auto_verify_email: data.auto_verify_email,
account_linking_enabled: data.account_linking_enabled,
enabled: data.enabled,
};
let oauth_provider = OAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?;
Ok(Json(oauth_provider))
}

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